KiCad PCB EDA Suite
Loading...
Searching...
No Matches
drc_benchmark.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 */
19
32
33#include <algorithm>
34#include <atomic>
35#include <chrono>
36#include <clocale>
37#include <cmath>
38#include <cstdio>
39#include <fstream>
40#include <map>
41#include <memory>
42#include <optional>
43#include <set>
44#include <sstream>
45#include <string>
46#include <thread>
47#include <vector>
48
49#include <wx/cmdline.h>
50#include <wx/filename.h>
51#include <wx/init.h>
52#include <wx/log.h>
53
54#include <core/profile.h>
55#include <pgm_base.h>
58#include <string_utils.h>
59#include <thread_pool.h>
61
62#include <board.h>
65#include <drc/drc_engine.h>
66#include <drc/drc_item.h>
68#include <pcb_marker.h>
69#include <project.h>
71
73
74#include "corpus.h"
75#include "trace_capture.h"
76
77
78namespace
79{
80
86struct BENCH_PGM : public PGM_BASE
87{
88 void MacOpenFile( const wxString& aFileName ) override {}
89};
90
91
92BENCH_PGM g_program;
93
94
96enum class RULES_VARIANT
97{
98 NONE,
99 DEFAULT,
100 HEAVY
101};
102
103
105enum class CACHE_MODE
106{
107 COLD,
108 WARM
109};
110
111
112struct BENCH_CONFIG
113{
114 RULES_VARIANT rulesVariant = RULES_VARIANT::DEFAULT;
115 CACHE_MODE cache = CACHE_MODE::COLD;
116 int threads = 0;
117 int repeat = 5;
118};
119
120
122struct RUN_SAMPLE
123{
124 double compileMs = 0.0;
125 double checkMs = 0.0;
126 double cacheGenMs = 0.0;
127 int violations = 0;
128 std::map<wxString, double> providerMs;
129 bool timedOut = false;
130 double fraction = 1.0;
131};
132
133
135struct STAT
136{
137 double median = 0.0;
138 double mad = 0.0;
139};
140
141
142STAT computeStat( std::vector<double> aValues )
143{
144 STAT stat;
145
146 if( aValues.empty() )
147 return stat;
148
149 std::sort( aValues.begin(), aValues.end() );
150
151 size_t n = aValues.size();
152 stat.median = ( n % 2 ) ? aValues[n / 2] : 0.5 * ( aValues[n / 2 - 1] + aValues[n / 2] );
153
154 std::vector<double> devs;
155 devs.reserve( n );
156
157 for( double v : aValues )
158 devs.push_back( std::fabs( v - stat.median ) );
159
160 std::sort( devs.begin(), devs.end() );
161 stat.mad = ( n % 2 ) ? devs[n / 2] : 0.5 * ( devs[n / 2 - 1] + devs[n / 2] );
162
163 return stat;
164}
165
166
172double readOneMinuteLoad()
173{
174 std::ifstream in( "/proc/loadavg" );
175
176 if( !in.is_open() )
177 return -1.0;
178
179 double load = -1.0;
180 in >> load;
181
182 return load;
183}
184
185
193void applyThreadConfig( int aThreads )
194{
195 size_t n = aThreads > 0 ? static_cast<size_t>( aThreads ) : 0;
196
197 Pgm().GetThreadPool().reset( n );
198
199 // The cached GetKiCadThreadPool() pointer still aims at the same PGM-owned pool
200 // object, but invalidating it keeps us honest with the documented contract and
201 // forces a fresh fetch on the next provider call.
203}
204
205
207wxFileName resolveRules( const wxFileName& aBoardName, const std::optional<wxString>& aDefaultRules,
208 const std::optional<wxString>& aHeavyRules, RULES_VARIANT aVariant )
209{
210 if( aVariant == RULES_VARIANT::NONE )
211 return wxFileName();
212
213 if( aVariant == RULES_VARIANT::HEAVY )
214 {
215 if( aHeavyRules )
216 return wxFileName( *aHeavyRules );
217
218 return wxFileName();
219 }
220
221 if( aDefaultRules )
222 return wxFileName( *aDefaultRules );
223
224 wxFileName sidecar( aBoardName );
225 sidecar.SetExt( FILEEXT::DesignRulesFileExtension );
226
227 if( sidecar.Exists() )
228 return sidecar;
229
230 return wxFileName();
231}
232
233
239std::unique_ptr<BOARD> loadBoard( const wxFileName& aBoardName, SETTINGS_MANAGER& aManager,
240 const wxFileName& aProjectName )
241{
242 std::unique_ptr<BOARD> board;
243
244 try
245 {
247 std::string( aBoardName.GetFullPath().ToUTF8() ) );
248 }
249 catch( const IO_ERROR& ioe )
250 {
251 std::printf( "error loading board: %s\n", TO_UTF8( ioe.What() ) );
252 return nullptr;
253 }
254
255 if( !board )
256 {
257 std::printf( "error: board failed to load\n" );
258 return nullptr;
259 }
260
261 if( aProjectName.Exists() )
262 board->SetProject( &aManager.Prj() );
263
264 board->BuildListOfNets();
265 board->BuildConnectivity();
266 board->GetLengthCalculation()->SynchronizeTuningProfileProperties();
267
268 if( board->GetProject() )
269 {
270 std::unordered_set<wxString> dummy;
271 board->SynchronizeComponentClasses( dummy );
272 }
273
274 return board;
275}
276
277
285const std::map<wxString, std::vector<int>>& providerErrorCodes()
286{
287 static const std::map<wxString, std::vector<int>> codes = {
288 { wxT( "annular_width" ), { DRCE_ANNULAR_WIDTH } },
289 { wxT( "copper width" ), { DRCE_CONNECTION_WIDTH } },
290 { wxT( "connectivity" ), { DRCE_DANGLING_TRACK, DRCE_DANGLING_VIA,
295 { wxT( "clearance" ), { DRCE_CLEARANCE, DRCE_HOLE_CLEARANCE,
298 { wxT( "courtyard_clearance" ), { DRCE_MALFORMED_COURTYARD, DRCE_MISSING_COURTYARD,
301 { wxT( "creepage" ), { DRCE_CREEPAGE } },
302 { wxT( "diff_pair_coupling" ), { DRCE_DIFF_PAIR_GAP_OUT_OF_RANGE,
304 { wxT( "disallow" ), { DRCE_ALLOWED_ITEMS, DRCE_TEXT_ON_EDGECUTS } },
305 { wxT( "edge_clearance" ), { DRCE_EDGE_CLEARANCE, DRCE_SILK_EDGE_CLEARANCE } },
306 { wxT( "footprint checks" ), { DRCE_FOOTPRINT_TYPE_MISMATCH, DRCE_PADSTACK,
308 { wxT( "hole_size" ), { DRCE_DRILL_OUT_OF_RANGE,
310 { wxT( "hole_to_hole_clearance" ), { DRCE_DRILLED_HOLES_COLOCATED,
312 { wxT( "length" ), { DRCE_LENGTH_OUT_OF_RANGE,
316 { wxT( "physical_clearance" ), { DRCE_CLEARANCE, DRCE_HOLE_CLEARANCE } },
317 { wxT( "silk_clearance" ), { DRCE_SILK_CLEARANCE, DRCE_SILK_MASK_CLEARANCE } },
318 { wxT( "sliver checker" ), { DRCE_COPPER_SLIVER } },
319 { wxT( "solder_mask_issues" ), { DRCE_SILK_MASK_CLEARANCE, DRCE_SOLDERMASK_BRIDGE } },
320 { wxT( "text_dimensions" ), { DRCE_TEXT_HEIGHT, DRCE_TEXT_THICKNESS } },
321 { wxT( "text_mirroring" ), { DRCE_MIRRORED_TEXT_ON_FRONT_LAYER,
323 { wxT( "angle" ), { DRCE_TRACK_ANGLE } },
324 { wxT( "segment_length" ), { DRCE_TRACK_SEGMENT_LENGTH } },
325 { wxT( "width" ), { DRCE_TRACK_WIDTH } },
326 { wxT( "diameter" ), { DRCE_VIA_DIAMETER } },
327 { wxT( "zone connections" ), { DRCE_STARVED_THERMAL } }
328 };
329
330 return codes;
331}
332
333
339bool applyIsolate( BOARD* aBoard, const wxString& aProvider )
340{
341 auto it = providerErrorCodes().find( aProvider );
342
343 if( it == providerErrorCodes().end() )
344 return false;
345
346 std::set<int> keep( it->second.begin(), it->second.end() );
347
349
350 for( int code = DRCE_FIRST; code <= DRCE_LAST; ++code )
351 {
352 if( !keep.count( code ) )
354 }
355
356 return true;
357}
358
359
361
370class BENCH_PROGRESS : public PROGRESS_REPORTER_BASE
371{
372public:
373 explicit BENCH_PROGRESS( double aTimeoutSec ) :
374 PROGRESS_REPORTER_BASE( 1 ),
375 m_enabled( aTimeoutSec > 0.0 ),
376 m_deadline( std::chrono::steady_clock::now()
377 + std::chrono::duration_cast<std::chrono::steady_clock::duration>(
378 std::chrono::duration<double>( aTimeoutSec ) ) )
379 {
380 }
381
382 bool TimedOut() const { return m_timedOut.load(); }
383
384protected:
385 bool updateUI() override
386 {
387 if( m_enabled && std::chrono::steady_clock::now() >= m_deadline )
388 {
389 m_timedOut.store( true );
390 m_cancelled.store( true );
391
392 return false;
393 }
394
395 return true;
396 }
397
398private:
399 bool m_enabled;
400 std::chrono::steady_clock::time_point m_deadline;
401 std::atomic_bool m_timedOut{ false };
402};
403
404
405double timeCompile( BOARD* aBoard, const wxFileName& aRulesFile )
406{
407 std::shared_ptr<DRC_ENGINE> engine =
408 std::make_shared<DRC_ENGINE>( aBoard, &aBoard->GetDesignSettings() );
409
410 aBoard->GetDesignSettings().m_DRCEngine = engine;
411
412 PROF_TIMER timer;
413 engine->InitEngine( aRulesFile );
414 timer.Stop();
415
416 return timer.msecs();
417}
418
419
424RUN_SAMPLE timeRun( BOARD* aBoard, const wxFileName& aRulesFile, double aTimeoutSec )
425{
426 RUN_SAMPLE sample;
427
428 std::shared_ptr<DRC_ENGINE> engine =
429 std::make_shared<DRC_ENGINE>( aBoard, &aBoard->GetDesignSettings() );
430
431 aBoard->GetDesignSettings().m_DRCEngine = engine;
432
433 std::atomic<int> violationCount{ 0 };
434
435 engine->SetViolationHandler(
436 [&]( const std::shared_ptr<DRC_ITEM>& aItem, const VECTOR2I& aPos, int aLayer,
437 const std::function<void( PCB_MARKER* )>& aCreateMarker )
438 {
439 violationCount.fetch_add( 1, std::memory_order_relaxed );
440 } );
441
442 PROF_TIMER compileTimer;
443 engine->InitEngine( aRulesFile );
444 compileTimer.Stop();
445 sample.compileMs = compileTimer.msecs();
446
447 // Total provider count anchors the "% complete" reported on a timeout. The profile log
448 // records one entry per provider that finished, so completed/total is how far the check got.
449 size_t totalProviders = engine->GetTestProviders().size();
450
451 // Install the profile-trace scraper for the duration of RunTests so the engine's
452 // per-provider "DRC provider ... took" lines land in our maps. The previous target is
453 // chained, so ordinary logging still reaches the console.
454 wxLog::AddTraceMask( wxT( "KICAD_DRC_PROFILE" ) );
455
456 DRC_PROFILE_LOG* profileLog = new DRC_PROFILE_LOG();
457 wxLog* prevTarget = wxLog::SetActiveTarget( profileLog );
458
459 // Deadline starts at the check phase so a slow rule compile spends its own time without
460 // eating the evaluator's budget. InitEngine is not cancellable, so the timeout only bounds
461 // RunTests, which is the long pole this tool exists to measure.
462 BENCH_PROGRESS progress( aTimeoutSec );
463 engine->SetProgressReporter( &progress );
464
465 PROF_TIMER checkTimer;
466
467 try
468 {
469 engine->RunTests( EDA_UNITS::MM, true, false );
470 }
471 catch( const std::exception& e )
472 {
473 std::printf( "error during RunTests: %s\n", e.what() );
474 }
475
476 checkTimer.Stop();
477
478 engine->SetProgressReporter( nullptr );
479 wxLog::SetActiveTarget( prevTarget );
480
481 sample.providerMs = profileLog->ProviderMs();
482
483 sample.timedOut = progress.TimedOut();
484
485 if( sample.timedOut )
486 sample.fraction = totalProviders > 0
487 ? static_cast<double>( sample.providerMs.size() )
488 / static_cast<double>( totalProviders )
489 : 0.0;
490
491 // Prefer the engine's own "DRC took" total as the check denominator since it brackets
492 // the same span the provider rows live in. Fall back to our outer timer if the trace
493 // line did not arrive (e.g. RunTests bailed early).
494 double engineTotal = profileLog->TotalMs();
495
496 sample.checkMs = engineTotal > 0.0 ? engineTotal : checkTimer.msecs();
497
498 double providerSum = 0.0;
499
500 for( const auto& [name, ms] : sample.providerMs )
501 providerSum += ms;
502
503 // Cache generation has no trace line of its own; it is everything the check spent that
504 // is not attributable to a provider. Clamp at zero to absorb timer jitter.
505 sample.cacheGenMs = std::max( 0.0, sample.checkMs - providerSum );
506
507 sample.violations = violationCount.load( std::memory_order_relaxed );
508
509 delete profileLog;
510
511 return sample;
512}
513
514
516struct SWEEP_RESULT
517{
518 BENCH_CONFIG config;
519 bool ran = false;
520 bool underLoad = false;
521 STAT compile;
522 STAT check;
523 STAT cacheGen;
524 int violations = 0;
525 bool timedOut = false;
526 double fraction = 1.0;
527 std::map<wxString, STAT> providerStats;
528};
529
530
539SWEEP_RESULT runConfig( const wxFileName& aBoardName, SETTINGS_MANAGER& aManager,
540 const wxFileName& aProjectName, const wxFileName& aRulesFile,
541 const BENCH_CONFIG& aConfig, const std::optional<wxString>& aIsolate,
542 double aMaxLoad, double aTimeoutSec )
543{
544 SWEEP_RESULT result;
545 result.config = aConfig;
546
547 applyThreadConfig( aConfig.threads );
548
549 std::vector<double> compileSamples;
550 std::vector<double> checkSamples;
551 std::vector<double> cacheSamples;
552 std::map<wxString, std::vector<double>> providerSamples;
553
554 std::unique_ptr<BOARD> warmBoard;
555
556 if( aConfig.cache == CACHE_MODE::WARM )
557 {
558 warmBoard = loadBoard( aBoardName, aManager, aProjectName );
559
560 if( !warmBoard )
561 return result;
562
563 if( aIsolate )
564 applyIsolate( warmBoard.get(), *aIsolate );
565 }
566
567 for( int i = 0; i <= aConfig.repeat; ++i )
568 {
569 BOARD* board = warmBoard.get();
570
571 std::unique_ptr<BOARD> coldBoard;
572
573 if( aConfig.cache == CACHE_MODE::COLD )
574 {
575 coldBoard = loadBoard( aBoardName, aManager, aProjectName );
576
577 if( !coldBoard )
578 return result;
579
580 if( aIsolate )
581 applyIsolate( coldBoard.get(), *aIsolate );
582
583 board = coldBoard.get();
584 }
585
586 // Re-check load right before a timed pass so a spike that arrives mid-sweep taints
587 // only the rows it actually touched instead of silently corrupting the medians.
588 if( aMaxLoad > 0.0 && i > 0 )
589 {
590 double load = readOneMinuteLoad();
591
592 if( load > aMaxLoad )
593 result.underLoad = true;
594 }
595
596 RUN_SAMPLE sample = timeRun( board, aRulesFile, aTimeoutSec );
597
598 // A timeout means this cell is too slow to be worth repeating; bail immediately so a
599 // single deadline does not cost timeout x repeat. Keep the compile number (it finished
600 // before the check deadline) so the row still reports useful compiler data.
601 if( sample.timedOut )
602 {
603 result.timedOut = true;
604 result.fraction = sample.fraction;
605
606 if( compileSamples.empty() )
607 compileSamples.push_back( sample.compileMs );
608
609 break;
610 }
611
612 // The first pass primes allocators and any lazily built board state, so discard it.
613 if( i > 0 )
614 {
615 compileSamples.push_back( sample.compileMs );
616 checkSamples.push_back( sample.checkMs );
617 cacheSamples.push_back( sample.cacheGenMs );
618 result.violations = sample.violations;
619
620 for( const auto& [name, ms] : sample.providerMs )
621 providerSamples[name].push_back( ms );
622 }
623 }
624
625 if( warmBoard )
626 warmBoard->SetProject( nullptr );
627
628 result.ran = true;
629 result.compile = computeStat( compileSamples );
630 result.check = computeStat( checkSamples );
631 result.cacheGen = computeStat( cacheSamples );
632
633 for( auto& [name, samples] : providerSamples )
634 result.providerStats[name] = computeStat( samples );
635
636 return result;
637}
638
639
647STAT runCompileOnly( const wxFileName& aBoardName, SETTINGS_MANAGER& aManager,
648 const wxFileName& aProjectName, const wxFileName& aRulesFile, int aRepeat )
649{
650 std::unique_ptr<BOARD> board = loadBoard( aBoardName, aManager, aProjectName );
651
652 if( !board )
653 return STAT();
654
655 std::vector<double> samples;
656
657 for( int i = 0; i <= aRepeat; ++i )
658 {
659 double ms = timeCompile( board.get(), aRulesFile );
660
661 if( i > 0 )
662 samples.push_back( ms );
663 }
664
665 board->SetProject( nullptr );
666
667 return computeStat( samples );
668}
669
670
671const char* variantName( RULES_VARIANT aVariant )
672{
673 switch( aVariant )
674 {
675 case RULES_VARIANT::NONE: return "none";
676 case RULES_VARIANT::DEFAULT: return "default";
677 case RULES_VARIANT::HEAVY: return "heavy";
678 }
679
680 return "?";
681}
682
683
684bool parseVariant( const wxString& aArg, RULES_VARIANT& aVariant )
685{
686 if( aArg == wxT( "none" ) )
687 aVariant = RULES_VARIANT::NONE;
688 else if( aArg == wxT( "default" ) )
689 aVariant = RULES_VARIANT::DEFAULT;
690 else if( aArg == wxT( "heavy" ) )
691 aVariant = RULES_VARIANT::HEAVY;
692 else
693 return false;
694
695 return true;
696}
697
698
700wxString slurp( const wxFileName& aFile )
701{
702 std::ifstream in( aFile.GetFullPath().fn_str() );
703
704 if( !in.is_open() )
705 return wxEmptyString;
706
707 std::stringstream buffer;
708 buffer << in.rdbuf();
709
710 return wxString::FromUTF8( buffer.str().c_str() );
711}
712
713
715std::string jsonEscape( const wxString& aStr )
716{
717 std::string utf8( aStr.utf8_str() );
718 std::string out;
719 out.reserve( utf8.size() + 8 );
720
721 for( char c : utf8 )
722 {
723 switch( c )
724 {
725 case '"': out += "\\\""; break;
726 case '\\': out += "\\\\"; break;
727 case '\n': out += "\\n"; break;
728 case '\r': out += "\\r"; break;
729 case '\t': out += "\\t"; break;
730 default: out += c; break;
731 }
732 }
733
734 return out;
735}
736
737
739struct COVERAGE_ROW
740{
741 wxString board;
742 std::set<DRC_CONSTRAINT_T> constraints;
743 std::set<wxString> predicates;
744};
745
746
756COVERAGE_ROW collectCoverage( const wxFileName& aBoardName, SETTINGS_MANAGER& aManager,
757 const wxFileName& aProjectName, const wxFileName& aRulesFile )
758{
759 COVERAGE_ROW row;
760 row.board = aBoardName.GetFullName();
761
762 std::unique_ptr<BOARD> board = loadBoard( aBoardName, aManager, aProjectName );
763
764 if( !board )
765 return row;
766
767 std::shared_ptr<DRC_ENGINE> engine =
768 std::make_shared<DRC_ENGINE>( board.get(), &board->GetDesignSettings() );
769
770 board->GetDesignSettings().m_DRCEngine = engine;
771 engine->InitEngine( aRulesFile );
772
774 {
775 if( engine->HasRulesForConstraintType( type ) )
776 row.constraints.insert( type );
777 }
778
779 if( aRulesFile.IsOk() && aRulesFile.Exists() )
780 {
781 for( const wxString& pred : ScanPredicatesInRules( slurp( aRulesFile ) ) )
782 row.predicates.insert( pred );
783 }
784
785 board->SetProject( nullptr );
786
787 return row;
788}
789
790
795void emitCoverage( const std::vector<COVERAGE_ROW>& aRows, const wxString& aOutDir )
796{
797 std::set<DRC_CONSTRAINT_T> coveredConstraints;
798 std::set<wxString> coveredPredicates;
799
800 for( const COVERAGE_ROW& row : aRows )
801 {
802 coveredConstraints.insert( row.constraints.begin(), row.constraints.end() );
803 coveredPredicates.insert( row.predicates.begin(), row.predicates.end() );
804 }
805
806 std::printf( "=== coverage matrix ===\n" );
807 std::printf( "%-28s %s\n", "board", "constraints / predicates" );
808 std::printf( "%-28s %s\n", "----------------------------",
809 "-------------------------------------" );
810
811 for( const COVERAGE_ROW& row : aRows )
812 {
813 std::string cons;
814
815 for( DRC_CONSTRAINT_T type : row.constraints )
816 {
817 if( !cons.empty() )
818 cons += ",";
819
820 cons += ConstraintTypeName( type );
821 }
822
823 std::string preds;
824
825 for( const wxString& pred : row.predicates )
826 {
827 if( !preds.empty() )
828 preds += ",";
829
830 preds += std::string( pred.utf8_str() );
831 }
832
833 std::printf( "%-28s C[%s] P[%s]\n",
834 static_cast<const char*>( row.board.utf8_str() ), cons.c_str(),
835 preds.c_str() );
836 }
837
838 std::vector<const char*> uncoveredConstraints;
839
841 {
842 if( !coveredConstraints.count( type ) )
843 uncoveredConstraints.push_back( ConstraintTypeName( type ) );
844 }
845
846 std::vector<wxString> uncoveredPredicates;
847
848 for( const wxString& pred : AllPredicateNames() )
849 {
850 if( !coveredPredicates.count( pred ) )
851 uncoveredPredicates.push_back( pred );
852 }
853
854 std::printf( "\nUNCOVERED constraints:" );
855
856 for( const char* name : uncoveredConstraints )
857 std::printf( " %s", name );
858
859 std::printf( "%s\n", uncoveredConstraints.empty() ? " (none)" : "" );
860
861 std::printf( "UNCOVERED predicates:" );
862
863 for( const wxString& pred : uncoveredPredicates )
864 std::printf( " %s", static_cast<const char*>( pred.utf8_str() ) );
865
866 std::printf( "%s\n\n", uncoveredPredicates.empty() ? " (none)" : "" );
867
868 wxFileName outFile( aOutDir, wxT( "coverage.json" ) );
869 std::ofstream out( outFile.GetFullPath().fn_str() );
870
871 if( !out.is_open() )
872 return;
873
874 out << "{\n \"boards\": [\n";
875
876 for( size_t i = 0; i < aRows.size(); ++i )
877 {
878 const COVERAGE_ROW& row = aRows[i];
879
880 out << " {\n \"board\": \"" << jsonEscape( row.board ) << "\",\n";
881 out << " \"constraints\": [";
882
883 bool first = true;
884
885 for( DRC_CONSTRAINT_T type : row.constraints )
886 {
887 out << ( first ? "" : ", " ) << "\"" << ConstraintTypeName( type ) << "\"";
888 first = false;
889 }
890
891 out << "],\n \"predicates\": [";
892
893 first = true;
894
895 for( const wxString& pred : row.predicates )
896 {
897 out << ( first ? "" : ", " ) << "\"" << jsonEscape( pred ) << "\"";
898 first = false;
899 }
900
901 out << "]\n }" << ( i + 1 < aRows.size() ? "," : "" ) << "\n";
902 }
903
904 out << " ],\n \"uncovered_constraints\": [";
905
906 for( size_t i = 0; i < uncoveredConstraints.size(); ++i )
907 out << ( i ? ", " : "" ) << "\"" << uncoveredConstraints[i] << "\"";
908
909 out << "],\n \"uncovered_predicates\": [";
910
911 for( size_t i = 0; i < uncoveredPredicates.size(); ++i )
912 out << ( i ? ", " : "" ) << "\"" << jsonEscape( uncoveredPredicates[i] ) << "\"";
913
914 out << "]\n}\n";
915}
916
917
919struct RESULT_ROW
920{
921 wxString board;
922 wxString config;
923 STAT compile;
924 STAT cacheGen;
925 STAT check;
926 double evalOverheadMs = 0.0;
927 bool evalOverheadValid = false;
928 int violations = 0;
929 bool underLoad = false;
930 bool timedOut = false;
931 double fraction = 1.0;
932 std::map<wxString, STAT> perProvider;
933};
934
935
936void writeResultsJson( const std::vector<RESULT_ROW>& aRows, const wxString& aOutDir )
937{
938 wxFileName outFile( aOutDir, wxT( "results.json" ) );
939 std::ofstream out( outFile.GetFullPath().fn_str() );
940
941 if( !out.is_open() )
942 return;
943
944 out << "[\n";
945
946 for( size_t i = 0; i < aRows.size(); ++i )
947 {
948 const RESULT_ROW& row = aRows[i];
949
950 out << " {\n";
951 out << " \"board\": \"" << jsonEscape( row.board ) << "\",\n";
952 out << " \"config\": \"" << jsonEscape( row.config ) << "\",\n";
953 out << " \"compile_ms\": " << row.compile.median << ",\n";
954 out << " \"compile_mad\": " << row.compile.mad << ",\n";
955 out << " \"cache_gen_ms\": " << row.cacheGen.median << ",\n";
956 out << " \"cache_gen_mad\": " << row.cacheGen.mad << ",\n";
957 out << " \"check_ms\": " << row.check.median << ",\n";
958 out << " \"check_mad\": " << row.check.mad << ",\n";
959
960 if( row.evalOverheadValid )
961 out << " \"eval_overhead_ms\": " << row.evalOverheadMs << ",\n";
962 else
963 out << " \"eval_overhead_ms\": null,\n";
964
965 out << " \"n_violations\": " << row.violations << ",\n";
966 out << " \"under_load\": " << ( row.underLoad ? "true" : "false" ) << ",\n";
967 out << " \"timed_out\": " << ( row.timedOut ? "true" : "false" ) << ",\n";
968 out << " \"percent_complete\": " << ( row.timedOut ? row.fraction * 100.0 : 100.0 )
969 << ",\n";
970 out << " \"per_provider\": {";
971
972 bool first = true;
973
974 for( const auto& [name, stat] : row.perProvider )
975 {
976 out << ( first ? "\n" : ",\n" );
977 out << " \"" << jsonEscape( name ) << "\": { \"median_ms\": " << stat.median
978 << ", \"mad_ms\": " << stat.mad << " }";
979 first = false;
980 }
981
982 out << ( first ? "}" : "\n }" ) << "\n";
983 out << " }" << ( i + 1 < aRows.size() ? "," : "" ) << "\n";
984 }
985
986 out << "]\n";
987}
988
989
995void writeWorstOffenders( const std::vector<RESULT_ROW>& aRows, const wxString& aOutDir, int aTopN )
996{
997 std::vector<const RESULT_ROW*> byCompile;
998 std::vector<const RESULT_ROW*> byEval;
999 std::vector<const RESULT_ROW*> timedOut;
1000
1001 for( const RESULT_ROW& row : aRows )
1002 {
1003 byCompile.push_back( &row );
1004
1005 if( row.evalOverheadValid )
1006 byEval.push_back( &row );
1007
1008 if( row.timedOut )
1009 timedOut.push_back( &row );
1010 }
1011
1012 std::sort( byCompile.begin(), byCompile.end(),
1013 []( const RESULT_ROW* a, const RESULT_ROW* b )
1014 {
1015 return a->compile.median > b->compile.median;
1016 } );
1017
1018 std::sort( byEval.begin(), byEval.end(),
1019 []( const RESULT_ROW* a, const RESULT_ROW* b )
1020 {
1021 return a->evalOverheadMs > b->evalOverheadMs;
1022 } );
1023
1024 // Least-complete first: a cell that only reached 10% before the deadline is a worse
1025 // offender than one that reached 90%.
1026 std::sort( timedOut.begin(), timedOut.end(),
1027 []( const RESULT_ROW* a, const RESULT_ROW* b )
1028 {
1029 return a->fraction < b->fraction;
1030 } );
1031
1032 auto emitList = [&]( const char* aLabel )
1033 {
1034 std::printf( "=== worst offenders by %s ===\n", aLabel );
1035 std::printf( "%-28s %-22s %12s\n", "board", "config", aLabel );
1036 std::printf( "%-28s %-22s %12s\n", "----------------------------",
1037 "----------------------", "------------" );
1038 };
1039
1040 emitList( "compile_ms" );
1041
1042 for( int i = 0; i < aTopN && i < static_cast<int>( byCompile.size() ); ++i )
1043 {
1044 const RESULT_ROW* row = byCompile[i];
1045
1046 std::printf( "%-28s %-22s %12.3f\n", static_cast<const char*>( row->board.utf8_str() ),
1047 static_cast<const char*>( row->config.utf8_str() ), row->compile.median );
1048 }
1049
1050 std::printf( "\n" );
1051 emitList( "eval_overhead_ms" );
1052
1053 for( int i = 0; i < aTopN && i < static_cast<int>( byEval.size() ); ++i )
1054 {
1055 const RESULT_ROW* row = byEval[i];
1056
1057 std::printf( "%-28s %-22s %12.3f\n", static_cast<const char*>( row->board.utf8_str() ),
1058 static_cast<const char*>( row->config.utf8_str() ), row->evalOverheadMs );
1059 }
1060
1061 std::printf( "\n" );
1062
1063 if( !timedOut.empty() )
1064 {
1065 std::printf( "=== timed out (eval unbounded; ranked least-complete first) ===\n" );
1066 std::printf( "%-28s %-22s %12s\n", "board", "config", "percent" );
1067 std::printf( "%-28s %-22s %12s\n", "----------------------------",
1068 "----------------------", "------------" );
1069
1070 for( const RESULT_ROW* row : timedOut )
1071 {
1072 std::printf( "%-28s %-22s %11.1f%%\n",
1073 static_cast<const char*>( row->board.utf8_str() ),
1074 static_cast<const char*>( row->config.utf8_str() ), row->fraction * 100.0 );
1075 }
1076
1077 std::printf( "\n" );
1078 }
1079
1080 wxFileName outFile( aOutDir, wxT( "worst_offenders.json" ) );
1081 std::ofstream out( outFile.GetFullPath().fn_str() );
1082
1083 if( !out.is_open() )
1084 return;
1085
1086 auto writeRanked = [&]( const char* aKey, const std::vector<const RESULT_ROW*>& aList,
1087 bool aUseEval )
1088 {
1089 out << " \"" << aKey << "\": [\n";
1090
1091 int count = std::min<int>( aTopN, static_cast<int>( aList.size() ) );
1092
1093 for( int i = 0; i < count; ++i )
1094 {
1095 const RESULT_ROW* row = aList[i];
1096 double value = aUseEval ? row->evalOverheadMs : row->compile.median;
1097
1098 out << " { \"board\": \"" << jsonEscape( row->board ) << "\", \"config\": \""
1099 << jsonEscape( row->config ) << "\", \"" << ( aUseEval ? "eval_overhead_ms"
1100 : "compile_ms" )
1101 << "\": " << value << " }" << ( i + 1 < count ? "," : "" ) << "\n";
1102 }
1103
1104 out << " ]";
1105 };
1106
1107 out << "{\n";
1108 writeRanked( "by_compile_ms", byCompile, false );
1109 out << ",\n";
1110 writeRanked( "by_eval_overhead_ms", byEval, true );
1111 out << ",\n";
1112
1113 out << " \"timed_out\": [\n";
1114
1115 for( size_t i = 0; i < timedOut.size(); ++i )
1116 {
1117 const RESULT_ROW* row = timedOut[i];
1118
1119 out << " { \"board\": \"" << jsonEscape( row->board ) << "\", \"config\": \""
1120 << jsonEscape( row->config ) << "\", \"percent_complete\": " << row->fraction * 100.0
1121 << " }" << ( i + 1 < timedOut.size() ? "," : "" ) << "\n";
1122 }
1123
1124 out << " ]\n}\n";
1125}
1126
1127
1129wxString configTag( CACHE_MODE aCache, RULES_VARIANT aVariant, int aThreads )
1130{
1131 return wxString::Format( wxT( "%s/%s/t%d" ), aCache == CACHE_MODE::COLD ? "cold" : "warm",
1132 variantName( aVariant ), aThreads );
1133}
1134
1135} // namespace
1136
1137
1138int main( int argc, char** argv )
1139{
1140 wxInitialize( argc, argv );
1141
1142 // Force the C locale so numeric parsing of the trace lines stays on a dot decimal
1143 // separator regardless of the environment.
1144 std::setlocale( LC_ALL, "C" );
1145
1146 // Line-buffer stdout so per-board progress streams to a redirected log during a long sweep
1147 // instead of materializing only at exit.
1148 std::setvbuf( stdout, nullptr, _IOLBF, 0 );
1149
1150 // The self-check exercises the trace parser alone and needs no engine, project, or
1151 // board, so handle it before any heavier initialization.
1152 for( int i = 1; i < argc; ++i )
1153 {
1154 if( std::string( argv[i] ) == "--selftest" )
1155 {
1156 int rv = RunTraceCaptureSelftest();
1157 wxUninitialize();
1158
1159 return rv;
1160 }
1161 }
1162
1163 SetPgm( &g_program );
1164 Pgm().InitPgm( true, true );
1165
1167 propMgr.Rebuild();
1168
1169 static const wxCmdLineEntryDesc cmdLineDesc[] = {
1170 { wxCMD_LINE_SWITCH, nullptr, "selftest", "run the trace-parser self-check and exit",
1171 wxCMD_LINE_VAL_NONE, wxCMD_LINE_PARAM_OPTIONAL },
1172 { wxCMD_LINE_OPTION, nullptr, "board", "ad-hoc board override (skips the corpus manifest)",
1173 wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL },
1174 { wxCMD_LINE_OPTION, "r", "rules", "default-variant design rules file (.kicad_dru)",
1175 wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL },
1176 { wxCMD_LINE_OPTION, nullptr, "heavy-rules", "heavy-variant design rules file (.kicad_dru)",
1177 wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL },
1178 { wxCMD_LINE_OPTION, nullptr, "rules-variant",
1179 "none|default|heavy (default: sweep all three)", wxCMD_LINE_VAL_STRING,
1180 wxCMD_LINE_PARAM_OPTIONAL },
1181 { wxCMD_LINE_SWITCH, nullptr, "rules-only",
1182 "time only InitEngine() compile in a loop, no checks", wxCMD_LINE_VAL_NONE,
1183 wxCMD_LINE_PARAM_OPTIONAL },
1184 { wxCMD_LINE_OPTION, nullptr, "threads", "worker threads, 0=all (default 0)",
1185 wxCMD_LINE_VAL_NUMBER, wxCMD_LINE_PARAM_OPTIONAL },
1186 { wxCMD_LINE_OPTION, nullptr, "repeat", "timed repeats after warm-up (default 5)",
1187 wxCMD_LINE_VAL_NUMBER, wxCMD_LINE_PARAM_OPTIONAL },
1188 { wxCMD_LINE_OPTION, nullptr, "cache", "cold|warm|both (default both)",
1189 wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL },
1190 { wxCMD_LINE_OPTION, nullptr, "isolate",
1191 "ignore every provider but this one to attribute its eval cost",
1192 wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL },
1193 { wxCMD_LINE_OPTION, nullptr, "top-n", "worst-offender list length (default 10)",
1194 wxCMD_LINE_VAL_NUMBER, wxCMD_LINE_PARAM_OPTIONAL },
1195 { wxCMD_LINE_OPTION, nullptr, "out", "output directory for JSON artifacts (default cwd)",
1196 wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL },
1197 { wxCMD_LINE_OPTION, nullptr, "max-load",
1198 "abort when 1-min loadavg exceeds this (default ncores*0.5)", wxCMD_LINE_VAL_STRING,
1199 wxCMD_LINE_PARAM_OPTIONAL },
1200 { wxCMD_LINE_OPTION, nullptr, "timeout",
1201 "per-test deadline in seconds; 0 disables (default 60)", wxCMD_LINE_VAL_NUMBER,
1202 wxCMD_LINE_PARAM_OPTIONAL },
1203 { wxCMD_LINE_SWITCH, nullptr, "quick",
1204 "fast iteration set: small/synthetic boards, warm none+heavy, repeat 3",
1205 wxCMD_LINE_VAL_NONE, wxCMD_LINE_PARAM_OPTIONAL },
1206 { wxCMD_LINE_OPTION, nullptr, "quick-max-mb",
1207 "in --quick, also keep real boards smaller than this many MB (default 0 = synthetic only)",
1208 wxCMD_LINE_VAL_NUMBER, wxCMD_LINE_PARAM_OPTIONAL },
1209 { wxCMD_LINE_PARAM, nullptr, nullptr, "board.kicad_pcb", wxCMD_LINE_VAL_STRING,
1210 wxCMD_LINE_PARAM_OPTIONAL },
1211 { wxCMD_LINE_NONE }
1212 };
1213
1214 wxCmdLineParser parser( argc, argv );
1215 parser.SetDesc( cmdLineDesc );
1216 parser.SetLogo( "qa_drc_benchmark: time DRC rule compile + expression evaluation" );
1217
1218 if( parser.Parse() != 0 )
1219 {
1220 Pgm().Destroy();
1221 wxUninitialize();
1222 return 1;
1223 }
1224
1226
1227 auto cleanupExit = [&]( int aCode )
1228 {
1229 Pgm().Destroy();
1230 wxUninitialize();
1231 return aCode;
1232 };
1233
1234 // Resolve which boards to run. An explicit --board or a positional board is an ad-hoc run;
1235 // otherwise the corpus manifest drives the sweep. An unconfigured corpus with no ad-hoc
1236 // board is a clean skip, never a failure.
1237 std::vector<CORPUS_ENTRY> entries;
1238
1239 wxString adHocBoard;
1240 bool haveAdHocBoard = parser.Found( "board", &adHocBoard );
1241
1242 if( !haveAdHocBoard && parser.GetParamCount() > 0 )
1243 {
1244 adHocBoard = parser.GetParam( 0 );
1245 haveAdHocBoard = true;
1246 }
1247
1248 std::optional<wxString> cliDefaultRules;
1249 wxString rulesArg;
1250
1251 if( parser.Found( "r", &rulesArg ) )
1252 cliDefaultRules = rulesArg;
1253
1254 std::optional<wxString> cliHeavyRules;
1255 wxString heavyArg;
1256
1257 if( parser.Found( "heavy-rules", &heavyArg ) )
1258 cliHeavyRules = heavyArg;
1259
1260 if( haveAdHocBoard )
1261 {
1262 CORPUS_ENTRY entry;
1263 wxFileName board( adHocBoard );
1264 board.MakeAbsolute();
1265 entry.board = board.GetFullPath();
1266
1267 if( cliDefaultRules )
1268 {
1269 wxFileName rules( *cliDefaultRules );
1270 rules.MakeAbsolute();
1271 entry.rules = rules.GetFullPath();
1272 }
1273
1274 entry.tier = wxT( "adhoc" );
1275 entries.push_back( entry );
1276 }
1277 else
1278 {
1279 if( !CORPUS::IsConfigured() )
1280 {
1281 std::printf( "KICAD_DRC_BENCH_CORPUS is unset or does not name a directory; "
1282 "nothing to benchmark.\n"
1283 "Set it to a corpus root containing corpus.json, or pass a board "
1284 "with --board <file.kicad_pcb>. Skipping.\n" );
1285 return cleanupExit( 0 );
1286 }
1287
1288 wxString loadError;
1289
1290 if( !CORPUS::Load( entries, loadError ) )
1291 {
1292 std::printf( "error loading corpus: %s\n",
1293 static_cast<const char*>( loadError.utf8_str() ) );
1294 return cleanupExit( 1 );
1295 }
1296
1297 if( entries.empty() )
1298 {
1299 std::printf( "corpus at %s has no entries; nothing to benchmark.\n",
1300 static_cast<const char*>( CORPUS::Root().utf8_str() ) );
1301 return cleanupExit( 0 );
1302 }
1303 }
1304
1305 long threadsArg = 0;
1306 int threads = 0;
1307
1308 if( parser.Found( "threads", &threadsArg ) )
1309 threads = static_cast<int>( threadsArg );
1310
1311 bool quick = parser.Found( "quick" );
1312
1313 double timeoutSec = 60.0;
1314 long timeoutArg = 60;
1315
1316 if( parser.Found( "timeout", &timeoutArg ) )
1317 timeoutSec = static_cast<double>( std::max( 0L, timeoutArg ) );
1318
1319 // Default 0 keeps the quick set to the curated synthetic stressors only; a positive value
1320 // opts small real boards back in for a slightly broader but slower iteration loop.
1321 double quickMaxMb = 0.0;
1322 long quickMaxArg = 0;
1323
1324 if( parser.Found( "quick-max-mb", &quickMaxArg ) )
1325 quickMaxMb = static_cast<double>( std::max( 0L, quickMaxArg ) );
1326
1327 long repeatArg = 5;
1328 int repeat = quick ? 3 : 5;
1329
1330 if( parser.Found( "repeat", &repeatArg ) )
1331 repeat = std::max( 1, static_cast<int>( repeatArg ) );
1332
1333 long topNArg = 10;
1334 int topN = 10;
1335
1336 if( parser.Found( "top-n", &topNArg ) )
1337 topN = std::max( 1, static_cast<int>( topNArg ) );
1338
1339 std::optional<wxString> isolate;
1340 wxString isolateArg;
1341
1342 if( parser.Found( "isolate", &isolateArg ) )
1343 {
1344 isolate = isolateArg;
1345
1346 if( !providerErrorCodes().count( isolateArg ) )
1347 std::printf( "warning: --isolate '%s' has no known error codes; the full provider "
1348 "set will run.\n",
1349 static_cast<const char*>( isolateArg.utf8_str() ) );
1350 }
1351
1352 wxString outDir = wxFileName::GetCwd();
1353 wxString outArg;
1354
1355 if( parser.Found( "out", &outArg ) )
1356 outDir = outArg;
1357
1358 unsigned ncores = std::max( 1u, std::thread::hardware_concurrency() );
1359 double maxLoad = ncores * 0.5;
1360 wxString maxLoadArg;
1361
1362 if( parser.Found( "max-load", &maxLoadArg ) )
1363 maxLoadArg.ToCDouble( &maxLoad );
1364
1365 // Low-load guard. The benchmark only means anything on an otherwise-idle machine, so refuse
1366 // to start under contention rather than emit noisy numbers the optimizer might trust.
1367 double startLoad = readOneMinuteLoad();
1368
1369 if( maxLoad > 0.0 && startLoad > maxLoad )
1370 {
1371 std::printf( "aborting: 1-min loadavg %.2f exceeds limit %.2f (cores=%u). "
1372 "Run on an idle machine or raise --max-load.\n",
1373 startLoad, maxLoad, ncores );
1374 return cleanupExit( 2 );
1375 }
1376
1377 std::vector<CACHE_MODE> cacheModes = { CACHE_MODE::COLD, CACHE_MODE::WARM };
1378 wxString cacheArg;
1379
1380 if( parser.Found( "cache", &cacheArg ) )
1381 {
1382 if( cacheArg == wxT( "cold" ) )
1383 cacheModes = { CACHE_MODE::COLD };
1384 else if( cacheArg == wxT( "warm" ) )
1385 cacheModes = { CACHE_MODE::WARM };
1386 else if( cacheArg != wxT( "both" ) )
1387 {
1388 std::printf( "error: --cache must be cold|warm|both\n" );
1389 return cleanupExit( 1 );
1390 }
1391 }
1392 else if( quick )
1393 {
1394 // Warm is the iteration-relevant number and halves the work versus cold+warm.
1395 cacheModes = { CACHE_MODE::WARM };
1396 }
1397
1398 std::vector<RULES_VARIANT> variants = { RULES_VARIANT::NONE, RULES_VARIANT::DEFAULT,
1399 RULES_VARIANT::HEAVY };
1400 wxString variantArg;
1401
1402 if( parser.Found( "rules-variant", &variantArg ) )
1403 {
1404 RULES_VARIANT v = RULES_VARIANT::DEFAULT;
1405
1406 if( !parseVariant( variantArg, v ) )
1407 {
1408 std::printf( "error: --rules-variant must be none|default|heavy\n" );
1409 return cleanupExit( 1 );
1410 }
1411
1412 variants = { v };
1413 }
1414 else if( quick )
1415 {
1416 // none gives the geometric baseline and heavy gives compile_ms plus the eval overhead
1417 // over that baseline; the default variant adds cost without new signal in quick mode.
1418 variants = { RULES_VARIANT::NONE, RULES_VARIANT::HEAVY };
1419 }
1420
1421 // In quick mode keep only the immediately meaningful boards: the curated fast cells that
1422 // expose compiler and evaluator cost in seconds. Honor explicit "quick" manifest flags when
1423 // present, otherwise fall back to the synthetic tier C. --quick-max-mb opts small real boards
1424 // back in. The multi-minute giants belong to the full sweep, not the iteration loop.
1425 if( quick && !haveAdHocBoard )
1426 {
1427 bool anyFlagged = std::any_of( entries.begin(), entries.end(),
1428 []( const CORPUS_ENTRY& e ) { return e.quick; } );
1429
1430 std::vector<CORPUS_ENTRY> kept;
1431
1432 for( const CORPUS_ENTRY& entry : entries )
1433 {
1434 wxULongLong size = wxFileName::GetSize( entry.board );
1435 bool small = quickMaxMb > 0.0 && size != wxInvalidSize
1436 && size.ToDouble() < quickMaxMb * 1.0e6;
1437 bool flagged = anyFlagged ? entry.quick : ( entry.tier == wxT( "C" ) );
1438
1439 if( flagged || small )
1440 kept.push_back( entry );
1441 }
1442
1443 entries.swap( kept );
1444 }
1445
1446 std::printf( "corpus: %s\n", haveAdHocBoard
1447 ? "ad-hoc"
1448 : static_cast<const char*>( CORPUS::Root().utf8_str() ) );
1449 std::printf( "boards: %zu%s\n", entries.size(), quick ? " (quick set)" : "" );
1450 std::printf( "threads: %d (0=all)\n", threads );
1451 std::printf( "repeat: %d\n", repeat );
1452 std::printf( "timeout: %.0f s/test%s\n", timeoutSec, timeoutSec > 0.0 ? "" : " (disabled)" );
1453 std::printf( "max-load: %.2f (start %.2f, cores %u)\n", maxLoad, startLoad, ncores );
1454
1455 if( isolate )
1456 std::printf( "isolate: %s\n", static_cast<const char*>( isolate->utf8_str() ) );
1457
1458 std::printf( "out: %s\n\n", static_cast<const char*>( outDir.utf8_str() ) );
1459
1460 int rv = 0;
1461 std::vector<COVERAGE_ROW> coverageRows;
1462 std::vector<RESULT_ROW> resultRows;
1463
1464 bool rulesOnly = parser.Found( "rules-only" );
1465
1466 for( const CORPUS_ENTRY& entry : entries )
1467 {
1468 wxFileName boardName( entry.board );
1469 wxFileName projectName( boardName );
1470 projectName.SetExt( FILEEXT::ProjectFileExtension );
1471
1472 // A single malformed corpus board must not take down the whole sweep, so isolate every
1473 // board's load + timing behind a catch that records the failure and moves on.
1474 try
1475 {
1476
1477 if( projectName.Exists() )
1478 manager.LoadProject( projectName.GetFullPath() );
1479
1480 // The manifest rules path feeds both the default and heavy variants; ad-hoc runs may
1481 // instead carry an explicit --rules. Heavy falls back to --heavy-rules when present.
1482 std::optional<wxString> entryDefaultRules;
1483
1484 if( !entry.rules.IsEmpty() )
1485 entryDefaultRules = entry.rules;
1486 else if( cliDefaultRules )
1487 entryDefaultRules = cliDefaultRules;
1488
1489 std::optional<wxString> entryHeavyRules = cliHeavyRules ? cliHeavyRules : entryDefaultRules;
1490
1491 std::printf( "### board: %s (tier %s)\n",
1492 static_cast<const char*>( boardName.GetFullName().utf8_str() ),
1493 static_cast<const char*>( entry.tier.utf8_str() ) );
1494
1495 // Coverage uses the default-variant rules so the matrix reflects the rule set the corpus
1496 // ships, independent of any heavy sibling used only for stress timing.
1497 wxFileName coverageRules =
1498 resolveRules( boardName, entryDefaultRules, entryHeavyRules, RULES_VARIANT::DEFAULT );
1499
1500 coverageRows.push_back(
1501 collectCoverage( boardName, manager, projectName, coverageRules ) );
1502
1503 if( rulesOnly )
1504 {
1505 std::printf( "%-10s %14s %12s\n", "variant", "compile_med", "compile_mad" );
1506 std::printf( "%-10s %14s %12s\n", "----------", "--------------", "------------" );
1507
1508 for( RULES_VARIANT variant : variants )
1509 {
1510 wxFileName rulesFile =
1511 resolveRules( boardName, entryDefaultRules, entryHeavyRules, variant );
1512
1513 STAT compile =
1514 runCompileOnly( boardName, manager, projectName, rulesFile, repeat );
1515
1516 std::printf( "%-10s %14.3f %12.3f\n", variantName( variant ), compile.median,
1517 compile.mad );
1518
1519 RESULT_ROW resultRow;
1520 resultRow.board = boardName.GetFullName();
1521 resultRow.config = configTag( CACHE_MODE::COLD, variant, threads ) + wxT( "/compile" );
1522 resultRow.compile = compile;
1523 resultRows.push_back( resultRow );
1524 }
1525
1526 std::printf( "\n" );
1527 continue;
1528 }
1529
1530 for( CACHE_MODE cache : cacheModes )
1531 {
1532 const char* cacheLabel = cache == CACHE_MODE::COLD ? "cold" : "warm";
1533
1534 std::printf( "--- cache: %s ---\n", cacheLabel );
1535 std::printf( "%-10s %12s %10s %12s %12s %12s %10s\n", "variant", "compile_ms", "(mad)",
1536 "cache_gen", "check_ms", "eval_ovhd", "violations" );
1537 std::printf( "%-10s %12s %10s %12s %12s %12s %10s\n", "----------", "------------",
1538 "----------", "------------", "------------", "------------",
1539 "----------" );
1540
1541 std::optional<double> noneCheck;
1542
1543 for( RULES_VARIANT variant : variants )
1544 {
1545 BENCH_CONFIG config;
1546 config.rulesVariant = variant;
1547 config.cache = cache;
1548 config.threads = threads;
1549 config.repeat = repeat;
1550
1551 wxFileName rulesFile =
1552 resolveRules( boardName, entryDefaultRules, entryHeavyRules, variant );
1553
1554 SWEEP_RESULT result = runConfig( boardName, manager, projectName, rulesFile, config,
1555 isolate, maxLoad, timeoutSec );
1556
1557 if( !result.ran )
1558 {
1559 rv = 1;
1560 continue;
1561 }
1562
1563 // A timed-out none baseline cannot anchor eval_overhead, so only record it when
1564 // the check actually completed.
1565 if( variant == RULES_VARIANT::NONE && !result.timedOut )
1566 noneCheck = result.check.median;
1567
1568 RESULT_ROW resultRow;
1569 resultRow.board = boardName.GetFullName();
1570 resultRow.config = configTag( cache, variant, threads );
1571 resultRow.compile = result.compile;
1572 resultRow.cacheGen = result.cacheGen;
1573 resultRow.check = result.check;
1574 resultRow.violations = result.violations;
1575 resultRow.underLoad = result.underLoad;
1576 resultRow.timedOut = result.timedOut;
1577 resultRow.fraction = result.fraction;
1578 resultRow.perProvider = result.providerStats;
1579
1580 // eval_overhead isolates what the evaluator adds over the rules-free geometric
1581 // baseline for the same board and cache mode. It only has meaning when both this
1582 // check and the none baseline completed in this same sweep; a timeout leaves the
1583 // check time partial, so the overhead stays unrecorded.
1584 wxString ovhd = wxT( "n/a" );
1585
1586 if( result.timedOut )
1587 {
1588 ovhd = wxT( "timeout" );
1589 }
1590 else if( variant == RULES_VARIANT::NONE )
1591 {
1592 resultRow.evalOverheadMs = 0.0;
1593 resultRow.evalOverheadValid = true;
1594 ovhd = wxT( "0.000" );
1595 }
1596 else if( noneCheck )
1597 {
1598 resultRow.evalOverheadMs = result.check.median - *noneCheck;
1599 resultRow.evalOverheadValid = true;
1600 ovhd = wxString::Format( wxT( "%.3f" ), resultRow.evalOverheadMs );
1601 }
1602
1603 resultRows.push_back( resultRow );
1604
1605 if( result.timedOut )
1606 {
1607 std::printf( "%-10s %12.3f %10.3f %12s %12s %12s [TIMEOUT %.0f%%]\n",
1608 variantName( variant ), result.compile.median, result.compile.mad,
1609 "-", "-", static_cast<const char*>( ovhd.utf8_str() ),
1610 result.fraction * 100.0 );
1611 }
1612 else
1613 {
1614 std::printf( "%-10s %12.3f %10.3f %12.3f %12.3f %12s %10d%s\n",
1615 variantName( variant ), result.compile.median, result.compile.mad,
1616 result.cacheGen.median, result.check.median,
1617 static_cast<const char*>( ovhd.utf8_str() ), result.violations,
1618 result.underLoad ? " [UNDER_LOAD]" : "" );
1619 }
1620 }
1621
1622 std::printf( "\n" );
1623 }
1624
1625 }
1626 catch( const std::exception& e )
1627 {
1628 std::printf( "error: board '%s' failed and was skipped: %s\n\n",
1629 static_cast<const char*>( boardName.GetFullName().utf8_str() ), e.what() );
1630 rv = 1;
1631 }
1632 }
1633
1634 emitCoverage( coverageRows, outDir );
1635 writeWorstOffenders( resultRows, outDir, topN );
1636 writeResultsJson( resultRows, outDir );
1637
1638 std::printf( "wrote results.json, worst_offenders.json, coverage.json to %s\n",
1639 static_cast<const char*>( outDir.utf8_str() ) );
1640
1641 return cleanupExit( rv );
1642}
const char * name
General utilities for PCB file IO for QA programs.
Container for design settings for a BOARD object.
std::map< int, SEVERITY > m_DRCSeverities
std::shared_ptr< DRC_ENGINE > m_DRCEngine
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:372
BOARD_DESIGN_SETTINGS & GetDesignSettings() const
Definition board.cpp:1149
static bool Load(std::vector< CORPUS_ENTRY > &aEntries, wxString &aError)
Parse <root>/corpus.json into resolved entries.
Definition corpus.cpp:74
static bool IsConfigured()
True when KICAD_DRC_BENCH_CORPUS is set and names an existing directory.
Definition corpus.cpp:31
static wxString Root()
The resolved corpus root, or an empty string when unconfigured.
Definition corpus.cpp:42
wxLog chain target that scrapes the engine's "KICAD_DRC_PROFILE" trace channel.
const std::map< wxString, double > & ProviderMs() const
double TotalMs() const
Hold an error message and may be used when throwing exceptions containing meaningful error messages.
virtual const wxString What() const
A composite of Problem() and Where()
Container for data for KiCad programs.
Definition pgm_base.h:102
void Destroy()
Definition pgm_base.cpp:183
BS::priority_thread_pool & GetThreadPool()
Definition pgm_base.cpp:144
bool InitPgm(bool aHeadless=false, bool aIsUnitTest=false)
Initialize this program.
Definition pgm_base.cpp:320
virtual SETTINGS_MANAGER & GetSettingsManager() const
Definition pgm_base.h:124
A small class to help profiling.
Definition profile.h:46
void Stop()
Save the time when this function was called, and set the counter stane to stop.
Definition profile.h:86
double msecs(bool aSinceLast=false)
Definition profile.h:147
This implements all the tricky bits for thread safety, but the GUI is left to derived classes.
Provide class metadata.Helper macro to map type hashes to names.
static PROPERTY_MANAGER & Instance()
void Rebuild()
Rebuild the list of all registered properties.
bool LoadProject(const wxString &aFullPath, bool aSetActive=true)
Load a project or sets up a new project with a specified path.
PROJECT & Prj() const
A helper while we are not MDI-capable – return the one and only project.
const char * ConstraintTypeName(DRC_CONSTRAINT_T aType)
Human-readable token for a DRC_CONSTRAINT_T, matching the .kicad_dru keyword where one exists.
Definition corpus.cpp:151
std::vector< wxString > ScanPredicatesInRules(const wxString &aRulesText)
Scan raw .kicad_dru text for occurrences of each registered predicate name.
Definition corpus.cpp:257
const std::vector< wxString > & AllPredicateNames()
Every pcbexpr predicate registered in pcbexpr_functions.cpp, used for textual coverage scans.
Definition corpus.cpp:230
const std::vector< DRC_CONSTRAINT_T > & AllConstraintTypes()
Every DRC_CONSTRAINT_T the engine can carry rules for, in enum order, excluding NULL_CONSTRAINT.
Definition corpus.cpp:201
@ DRCE_SKEW_OUT_OF_RANGE
Definition drc_item.h:104
@ DRCE_DIFF_PAIR_GAP_OUT_OF_RANGE
Definition drc_item.h:106
@ DRCE_CREEPAGE
Definition drc_item.h:41
@ DRCE_HOLE_CLEARANCE
Definition drc_item.h:51
@ DRCE_SILK_EDGE_CLEARANCE
Definition drc_item.h:96
@ DRCE_ZONES_INTERSECT
Definition drc_item.h:44
@ DRCE_SILK_MASK_CLEARANCE
Definition drc_item.h:94
@ DRCE_VIA_DIAMETER
Definition drc_item.h:58
@ DRCE_UNCONNECTED_ITEMS
Definition drc_item.h:36
@ DRCE_TRACK_WIDTH
Definition drc_item.h:52
@ DRCE_PADSTACK
Definition drc_item.h:59
@ DRCE_MIRRORED_TEXT_ON_FRONT_LAYER
Definition drc_item.h:109
@ DRCE_OVERLAPPING_FOOTPRINTS
Definition drc_item.h:62
@ DRCE_TRACK_ON_POST_MACHINED_LAYER
Definition drc_item.h:115
@ DRCE_TEXT_ON_EDGECUTS
Definition drc_item.h:39
@ DRCE_NET_CHAIN_STUB_TOO_LONG
Definition drc_item.h:102
@ DRCE_DRILL_OUT_OF_RANGE
Definition drc_item.h:57
@ DRCE_EDGE_CLEARANCE
Definition drc_item.h:43
@ DRCE_NET_CHAIN_RETURN_PATH_BREAK
Definition drc_item.h:103
@ DRCE_STARVED_THERMAL
Definition drc_item.h:46
@ DRCE_TRACK_SEGMENT_LENGTH
Definition drc_item.h:54
@ DRCE_MISSING_COURTYARD
Definition drc_item.h:63
@ DRCE_TRACK_ANGLE
Definition drc_item.h:53
@ DRCE_TRACK_NOT_CENTERED_ON_VIA
Definition drc_item.h:117
@ DRCE_CLEARANCE
Definition drc_item.h:40
@ DRCE_ISOLATED_COPPER
Definition drc_item.h:45
@ DRCE_DRILLED_HOLES_TOO_CLOSE
Definition drc_item.h:49
@ DRCE_ALLOWED_ITEMS
Definition drc_item.h:38
@ DRCE_COPPER_SLIVER
Definition drc_item.h:90
@ DRCE_PTH_IN_COURTYARD
Definition drc_item.h:66
@ DRCE_DIFF_PAIR_UNCOUPLED_LENGTH_TOO_LONG
Definition drc_item.h:107
@ DRCE_MICROVIA_DRILL_OUT_OF_RANGE
Definition drc_item.h:61
@ DRCE_SHORTING_ITEMS
Definition drc_item.h:37
@ DRCE_MALFORMED_COURTYARD
Definition drc_item.h:64
@ DRCE_FIRST
Definition drc_item.h:35
@ DRCE_DANGLING_VIA
Definition drc_item.h:47
@ DRCE_FOOTPRINT_TYPE_MISMATCH
Definition drc_item.h:78
@ DRCE_NONMIRRORED_TEXT_ON_BACK_LAYER
Definition drc_item.h:110
@ DRCE_DANGLING_TRACK
Definition drc_item.h:48
@ DRCE_TEXT_HEIGHT
Definition drc_item.h:98
@ DRCE_SOLDERMASK_BRIDGE
Definition drc_item.h:91
@ DRCE_DRILLED_HOLES_COLOCATED
Definition drc_item.h:50
@ DRCE_SILK_CLEARANCE
Definition drc_item.h:97
@ DRCE_LENGTH_OUT_OF_RANGE
Definition drc_item.h:101
@ DRCE_LAST
Definition drc_item.h:121
@ DRCE_PAD_TH_WITH_NO_HOLE
Definition drc_item.h:81
@ DRCE_TEXT_THICKNESS
Definition drc_item.h:99
@ DRCE_NPTH_IN_COURTYARD
Definition drc_item.h:67
@ DRCE_CONNECTION_WIDTH
Definition drc_item.h:56
@ DRCE_TRACKS_CROSSING
Definition drc_item.h:42
@ DRCE_VIA_COUNT_OUT_OF_RANGE
Definition drc_item.h:105
@ DRCE_ANNULAR_WIDTH
Definition drc_item.h:55
DRC_CONSTRAINT_T
Definition drc_rule.h:49
@ NONE
Definition eda_shape.h:72
static const std::string ProjectFileExtension
static const std::string DesignRulesFileExtension
std::unique_ptr< BOARD > ReadBoardFromFileOrStream(const std::string &aFilename, std::istream &aFallback)
Read a board from a file, or another stream, as appropriate.
std::chrono::duration< double, std::milli > ms
void SetPgm(PGM_BASE *pgm)
PGM_BASE & Pgm()
The global program "get" accessor.
see class PGM_BASE
@ RPT_SEVERITY_IGNORE
std::vector< FAB_LAYER_COLOR > dummy
#define TO_UTF8(wxstring)
Convert a wxString to a UTF8 encoded C string for all wxWidgets build modes.
One board+rules pairing from the out-of-tree corpus manifest.
Definition corpus.h:36
wxString tier
Free-form tier tag from the manifest (A/B/C).
Definition corpus.h:39
wxString board
Absolute path to the .kicad_pcb.
Definition corpus.h:37
wxString rules
Absolute path to the .kicad_dru, or empty for none.
Definition corpus.h:38
VECTOR2I end
wxString result
Test unit parsing edge cases and error handling.
void InvalidateKiCadThreadPool()
Invalidate the cached thread pool pointer.
int RunTraceCaptureSelftest()
Run the three fixed self-test lines through a fresh parser and confirm the parsed provider map and to...
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683
Definition of file extensions used in Kicad.