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,
297 { wxT( "courtyard_clearance" ), { DRCE_MALFORMED_COURTYARD, DRCE_MISSING_COURTYARD,
300 { wxT( "creepage" ), { DRCE_CREEPAGE } },
301 { wxT( "diff_pair_coupling" ), { DRCE_DP_GAP_OUT_OF_RANGE, DRCE_DP_UNCOUPLED_LENGTH_TOO_LONG } },
302 { wxT( "disallow" ), { DRCE_ALLOWED_ITEMS, DRCE_TEXT_ON_EDGECUTS } },
303 { wxT( "edge_clearance" ), { DRCE_EDGE_CLEARANCE, DRCE_SILK_EDGE_CLEARANCE } },
304 { wxT( "footprint checks" ), { DRCE_FOOTPRINT_TYPE_MISMATCH, DRCE_PADSTACK,
307 DRCE_PADSTACK } },
308 { wxT( "hole_to_hole_clearance" ), { DRCE_DRILLED_HOLES_COLOCATED, DRCE_DRILLED_HOLES_TOO_CLOSE } },
312 { wxT( "physical_clearance" ), { DRCE_CLEARANCE, DRCE_HOLE_CLEARANCE } },
313 { wxT( "silk_clearance" ), { DRCE_SILK_CLEARANCE, DRCE_SILK_MASK_CLEARANCE } },
314 { wxT( "sliver checker" ), { DRCE_COPPER_SLIVER } },
315 { wxT( "solder_mask_issues" ), { DRCE_SILK_MASK_CLEARANCE, DRCE_SOLDERMASK_BRIDGE } },
316 { wxT( "text_dimensions" ), { DRCE_TEXT_HEIGHT, DRCE_TEXT_THICKNESS } },
318 { wxT( "angle" ), { DRCE_TRACK_ANGLE } },
319 { wxT( "segment_length" ), { DRCE_TRACK_SEGMENT_LENGTH } },
320 { wxT( "width" ), { DRCE_TRACK_WIDTH } },
321 { wxT( "diameter" ), { DRCE_VIA_DIAMETER } },
322 { wxT( "zone connections" ), { DRCE_STARVED_THERMAL } }
323 };
324
325 return codes;
326}
327
328
334bool applyIsolate( BOARD* aBoard, const wxString& aProvider )
335{
336 auto it = providerErrorCodes().find( aProvider );
337
338 if( it == providerErrorCodes().end() )
339 return false;
340
341 std::set<int> keep( it->second.begin(), it->second.end() );
342
344
345 for( int code = DRCE_FIRST; code <= DRCE_LAST; ++code )
346 {
347 if( !keep.count( code ) )
349 }
350
351 return true;
352}
353
354
356
365class BENCH_PROGRESS : public PROGRESS_REPORTER_BASE
366{
367public:
368 explicit BENCH_PROGRESS( double aTimeoutSec ) :
369 PROGRESS_REPORTER_BASE( 1 ),
370 m_enabled( aTimeoutSec > 0.0 ),
371 m_deadline( std::chrono::steady_clock::now()
372 + std::chrono::duration_cast<std::chrono::steady_clock::duration>(
373 std::chrono::duration<double>( aTimeoutSec ) ) )
374 {
375 }
376
377 bool TimedOut() const { return m_timedOut.load(); }
378
379protected:
380 bool updateUI() override
381 {
382 if( m_enabled && std::chrono::steady_clock::now() >= m_deadline )
383 {
384 m_timedOut.store( true );
385 m_cancelled.store( true );
386
387 return false;
388 }
389
390 return true;
391 }
392
393private:
394 bool m_enabled;
395 std::chrono::steady_clock::time_point m_deadline;
396 std::atomic_bool m_timedOut{ false };
397};
398
399
400double timeCompile( BOARD* aBoard, const wxFileName& aRulesFile )
401{
402 std::shared_ptr<DRC_ENGINE> engine =
403 std::make_shared<DRC_ENGINE>( aBoard, &aBoard->GetDesignSettings() );
404
405 aBoard->GetDesignSettings().m_DRCEngine = engine;
406
407 PROF_TIMER timer;
408 engine->InitEngine( aRulesFile );
409 timer.Stop();
410
411 return timer.msecs();
412}
413
414
419RUN_SAMPLE timeRun( BOARD* aBoard, const wxFileName& aRulesFile, double aTimeoutSec )
420{
421 RUN_SAMPLE sample;
422
423 std::shared_ptr<DRC_ENGINE> engine =
424 std::make_shared<DRC_ENGINE>( aBoard, &aBoard->GetDesignSettings() );
425
426 aBoard->GetDesignSettings().m_DRCEngine = engine;
427
428 std::atomic<int> violationCount{ 0 };
429
430 engine->SetViolationHandler(
431 [&]( const std::shared_ptr<DRC_ITEM>& aItem, const VECTOR2I& aPos, int aLayer,
432 const std::function<void( PCB_MARKER* )>& aCreateMarker )
433 {
434 violationCount.fetch_add( 1, std::memory_order_relaxed );
435 } );
436
437 PROF_TIMER compileTimer;
438 engine->InitEngine( aRulesFile );
439 compileTimer.Stop();
440 sample.compileMs = compileTimer.msecs();
441
442 // Total provider count anchors the "% complete" reported on a timeout. The profile log
443 // records one entry per provider that finished, so completed/total is how far the check got.
444 size_t totalProviders = engine->GetTestProviders().size();
445
446 // Install the profile-trace scraper for the duration of RunTests so the engine's
447 // per-provider "DRC provider ... took" lines land in our maps. The previous target is
448 // chained, so ordinary logging still reaches the console.
449 wxLog::AddTraceMask( wxT( "KICAD_DRC_PROFILE" ) );
450
451 DRC_PROFILE_LOG* profileLog = new DRC_PROFILE_LOG();
452 wxLog* prevTarget = wxLog::SetActiveTarget( profileLog );
453
454 // Deadline starts at the check phase so a slow rule compile spends its own time without
455 // eating the evaluator's budget. InitEngine is not cancellable, so the timeout only bounds
456 // RunTests, which is the long pole this tool exists to measure.
457 BENCH_PROGRESS progress( aTimeoutSec );
458 engine->SetProgressReporter( &progress );
459
460 PROF_TIMER checkTimer;
461
462 try
463 {
464 engine->RunTests( EDA_UNITS::MM, true, false );
465 }
466 catch( const std::exception& e )
467 {
468 std::printf( "error during RunTests: %s\n", e.what() );
469 }
470
471 checkTimer.Stop();
472
473 engine->SetProgressReporter( nullptr );
474 wxLog::SetActiveTarget( prevTarget );
475
476 sample.providerMs = profileLog->ProviderMs();
477
478 sample.timedOut = progress.TimedOut();
479
480 if( sample.timedOut )
481 sample.fraction = totalProviders > 0
482 ? static_cast<double>( sample.providerMs.size() )
483 / static_cast<double>( totalProviders )
484 : 0.0;
485
486 // Prefer the engine's own "DRC took" total as the check denominator since it brackets
487 // the same span the provider rows live in. Fall back to our outer timer if the trace
488 // line did not arrive (e.g. RunTests bailed early).
489 double engineTotal = profileLog->TotalMs();
490
491 sample.checkMs = engineTotal > 0.0 ? engineTotal : checkTimer.msecs();
492
493 double providerSum = 0.0;
494
495 for( const auto& [name, ms] : sample.providerMs )
496 providerSum += ms;
497
498 // Cache generation has no trace line of its own; it is everything the check spent that
499 // is not attributable to a provider. Clamp at zero to absorb timer jitter.
500 sample.cacheGenMs = std::max( 0.0, sample.checkMs - providerSum );
501
502 sample.violations = violationCount.load( std::memory_order_relaxed );
503
504 delete profileLog;
505
506 return sample;
507}
508
509
511struct SWEEP_RESULT
512{
513 BENCH_CONFIG config;
514 bool ran = false;
515 bool underLoad = false;
516 STAT compile;
517 STAT check;
518 STAT cacheGen;
519 int violations = 0;
520 bool timedOut = false;
521 double fraction = 1.0;
522 std::map<wxString, STAT> providerStats;
523};
524
525
534SWEEP_RESULT runConfig( const wxFileName& aBoardName, SETTINGS_MANAGER& aManager,
535 const wxFileName& aProjectName, const wxFileName& aRulesFile,
536 const BENCH_CONFIG& aConfig, const std::optional<wxString>& aIsolate,
537 double aMaxLoad, double aTimeoutSec )
538{
539 SWEEP_RESULT result;
540 result.config = aConfig;
541
542 applyThreadConfig( aConfig.threads );
543
544 std::vector<double> compileSamples;
545 std::vector<double> checkSamples;
546 std::vector<double> cacheSamples;
547 std::map<wxString, std::vector<double>> providerSamples;
548
549 std::unique_ptr<BOARD> warmBoard;
550
551 if( aConfig.cache == CACHE_MODE::WARM )
552 {
553 warmBoard = loadBoard( aBoardName, aManager, aProjectName );
554
555 if( !warmBoard )
556 return result;
557
558 if( aIsolate )
559 applyIsolate( warmBoard.get(), *aIsolate );
560 }
561
562 for( int i = 0; i <= aConfig.repeat; ++i )
563 {
564 BOARD* board = warmBoard.get();
565
566 std::unique_ptr<BOARD> coldBoard;
567
568 if( aConfig.cache == CACHE_MODE::COLD )
569 {
570 coldBoard = loadBoard( aBoardName, aManager, aProjectName );
571
572 if( !coldBoard )
573 return result;
574
575 if( aIsolate )
576 applyIsolate( coldBoard.get(), *aIsolate );
577
578 board = coldBoard.get();
579 }
580
581 // Re-check load right before a timed pass so a spike that arrives mid-sweep taints
582 // only the rows it actually touched instead of silently corrupting the medians.
583 if( aMaxLoad > 0.0 && i > 0 )
584 {
585 double load = readOneMinuteLoad();
586
587 if( load > aMaxLoad )
588 result.underLoad = true;
589 }
590
591 RUN_SAMPLE sample = timeRun( board, aRulesFile, aTimeoutSec );
592
593 // A timeout means this cell is too slow to be worth repeating; bail immediately so a
594 // single deadline does not cost timeout x repeat. Keep the compile number (it finished
595 // before the check deadline) so the row still reports useful compiler data.
596 if( sample.timedOut )
597 {
598 result.timedOut = true;
599 result.fraction = sample.fraction;
600
601 if( compileSamples.empty() )
602 compileSamples.push_back( sample.compileMs );
603
604 break;
605 }
606
607 // The first pass primes allocators and any lazily built board state, so discard it.
608 if( i > 0 )
609 {
610 compileSamples.push_back( sample.compileMs );
611 checkSamples.push_back( sample.checkMs );
612 cacheSamples.push_back( sample.cacheGenMs );
613 result.violations = sample.violations;
614
615 for( const auto& [name, ms] : sample.providerMs )
616 providerSamples[name].push_back( ms );
617 }
618 }
619
620 if( warmBoard )
621 warmBoard->SetProject( nullptr );
622
623 result.ran = true;
624 result.compile = computeStat( compileSamples );
625 result.check = computeStat( checkSamples );
626 result.cacheGen = computeStat( cacheSamples );
627
628 for( auto& [name, samples] : providerSamples )
629 result.providerStats[name] = computeStat( samples );
630
631 return result;
632}
633
634
642STAT runCompileOnly( const wxFileName& aBoardName, SETTINGS_MANAGER& aManager,
643 const wxFileName& aProjectName, const wxFileName& aRulesFile, int aRepeat )
644{
645 std::unique_ptr<BOARD> board = loadBoard( aBoardName, aManager, aProjectName );
646
647 if( !board )
648 return STAT();
649
650 std::vector<double> samples;
651
652 for( int i = 0; i <= aRepeat; ++i )
653 {
654 double ms = timeCompile( board.get(), aRulesFile );
655
656 if( i > 0 )
657 samples.push_back( ms );
658 }
659
660 board->SetProject( nullptr );
661
662 return computeStat( samples );
663}
664
665
666const char* variantName( RULES_VARIANT aVariant )
667{
668 switch( aVariant )
669 {
670 case RULES_VARIANT::NONE: return "none";
671 case RULES_VARIANT::DEFAULT: return "default";
672 case RULES_VARIANT::HEAVY: return "heavy";
673 }
674
675 return "?";
676}
677
678
679bool parseVariant( const wxString& aArg, RULES_VARIANT& aVariant )
680{
681 if( aArg == wxT( "none" ) )
682 aVariant = RULES_VARIANT::NONE;
683 else if( aArg == wxT( "default" ) )
684 aVariant = RULES_VARIANT::DEFAULT;
685 else if( aArg == wxT( "heavy" ) )
686 aVariant = RULES_VARIANT::HEAVY;
687 else
688 return false;
689
690 return true;
691}
692
693
695wxString slurp( const wxFileName& aFile )
696{
697 std::ifstream in( aFile.GetFullPath().fn_str() );
698
699 if( !in.is_open() )
700 return wxEmptyString;
701
702 std::stringstream buffer;
703 buffer << in.rdbuf();
704
705 return wxString::FromUTF8( buffer.str().c_str() );
706}
707
708
710std::string jsonEscape( const wxString& aStr )
711{
712 std::string utf8( aStr.utf8_str() );
713 std::string out;
714 out.reserve( utf8.size() + 8 );
715
716 for( char c : utf8 )
717 {
718 switch( c )
719 {
720 case '"': out += "\\\""; break;
721 case '\\': out += "\\\\"; break;
722 case '\n': out += "\\n"; break;
723 case '\r': out += "\\r"; break;
724 case '\t': out += "\\t"; break;
725 default: out += c; break;
726 }
727 }
728
729 return out;
730}
731
732
734struct COVERAGE_ROW
735{
736 wxString board;
737 std::set<DRC_CONSTRAINT_T> constraints;
738 std::set<wxString> predicates;
739};
740
741
751COVERAGE_ROW collectCoverage( const wxFileName& aBoardName, SETTINGS_MANAGER& aManager,
752 const wxFileName& aProjectName, const wxFileName& aRulesFile )
753{
754 COVERAGE_ROW row;
755 row.board = aBoardName.GetFullName();
756
757 std::unique_ptr<BOARD> board = loadBoard( aBoardName, aManager, aProjectName );
758
759 if( !board )
760 return row;
761
762 std::shared_ptr<DRC_ENGINE> engine =
763 std::make_shared<DRC_ENGINE>( board.get(), &board->GetDesignSettings() );
764
765 board->GetDesignSettings().m_DRCEngine = engine;
766 engine->InitEngine( aRulesFile );
767
769 {
770 if( engine->HasRulesForConstraintType( type ) )
771 row.constraints.insert( type );
772 }
773
774 if( aRulesFile.IsOk() && aRulesFile.Exists() )
775 {
776 for( const wxString& pred : ScanPredicatesInRules( slurp( aRulesFile ) ) )
777 row.predicates.insert( pred );
778 }
779
780 board->SetProject( nullptr );
781
782 return row;
783}
784
785
790void emitCoverage( const std::vector<COVERAGE_ROW>& aRows, const wxString& aOutDir )
791{
792 std::set<DRC_CONSTRAINT_T> coveredConstraints;
793 std::set<wxString> coveredPredicates;
794
795 for( const COVERAGE_ROW& row : aRows )
796 {
797 coveredConstraints.insert( row.constraints.begin(), row.constraints.end() );
798 coveredPredicates.insert( row.predicates.begin(), row.predicates.end() );
799 }
800
801 std::printf( "=== coverage matrix ===\n" );
802 std::printf( "%-28s %s\n", "board", "constraints / predicates" );
803 std::printf( "%-28s %s\n", "----------------------------",
804 "-------------------------------------" );
805
806 for( const COVERAGE_ROW& row : aRows )
807 {
808 std::string cons;
809
810 for( DRC_CONSTRAINT_T type : row.constraints )
811 {
812 if( !cons.empty() )
813 cons += ",";
814
815 cons += ConstraintTypeName( type );
816 }
817
818 std::string preds;
819
820 for( const wxString& pred : row.predicates )
821 {
822 if( !preds.empty() )
823 preds += ",";
824
825 preds += std::string( pred.utf8_str() );
826 }
827
828 std::printf( "%-28s C[%s] P[%s]\n",
829 static_cast<const char*>( row.board.utf8_str() ), cons.c_str(),
830 preds.c_str() );
831 }
832
833 std::vector<const char*> uncoveredConstraints;
834
836 {
837 if( !coveredConstraints.count( type ) )
838 uncoveredConstraints.push_back( ConstraintTypeName( type ) );
839 }
840
841 std::vector<wxString> uncoveredPredicates;
842
843 for( const wxString& pred : AllPredicateNames() )
844 {
845 if( !coveredPredicates.count( pred ) )
846 uncoveredPredicates.push_back( pred );
847 }
848
849 std::printf( "\nUNCOVERED constraints:" );
850
851 for( const char* name : uncoveredConstraints )
852 std::printf( " %s", name );
853
854 std::printf( "%s\n", uncoveredConstraints.empty() ? " (none)" : "" );
855
856 std::printf( "UNCOVERED predicates:" );
857
858 for( const wxString& pred : uncoveredPredicates )
859 std::printf( " %s", static_cast<const char*>( pred.utf8_str() ) );
860
861 std::printf( "%s\n\n", uncoveredPredicates.empty() ? " (none)" : "" );
862
863 wxFileName outFile( aOutDir, wxT( "coverage.json" ) );
864 std::ofstream out( outFile.GetFullPath().fn_str() );
865
866 if( !out.is_open() )
867 return;
868
869 out << "{\n \"boards\": [\n";
870
871 for( size_t i = 0; i < aRows.size(); ++i )
872 {
873 const COVERAGE_ROW& row = aRows[i];
874
875 out << " {\n \"board\": \"" << jsonEscape( row.board ) << "\",\n";
876 out << " \"constraints\": [";
877
878 bool first = true;
879
880 for( DRC_CONSTRAINT_T type : row.constraints )
881 {
882 out << ( first ? "" : ", " ) << "\"" << ConstraintTypeName( type ) << "\"";
883 first = false;
884 }
885
886 out << "],\n \"predicates\": [";
887
888 first = true;
889
890 for( const wxString& pred : row.predicates )
891 {
892 out << ( first ? "" : ", " ) << "\"" << jsonEscape( pred ) << "\"";
893 first = false;
894 }
895
896 out << "]\n }" << ( i + 1 < aRows.size() ? "," : "" ) << "\n";
897 }
898
899 out << " ],\n \"uncovered_constraints\": [";
900
901 for( size_t i = 0; i < uncoveredConstraints.size(); ++i )
902 out << ( i ? ", " : "" ) << "\"" << uncoveredConstraints[i] << "\"";
903
904 out << "],\n \"uncovered_predicates\": [";
905
906 for( size_t i = 0; i < uncoveredPredicates.size(); ++i )
907 out << ( i ? ", " : "" ) << "\"" << jsonEscape( uncoveredPredicates[i] ) << "\"";
908
909 out << "]\n}\n";
910}
911
912
914struct RESULT_ROW
915{
916 wxString board;
917 wxString config;
918 STAT compile;
919 STAT cacheGen;
920 STAT check;
921 double evalOverheadMs = 0.0;
922 bool evalOverheadValid = false;
923 int violations = 0;
924 bool underLoad = false;
925 bool timedOut = false;
926 double fraction = 1.0;
927 std::map<wxString, STAT> perProvider;
928};
929
930
931void writeResultsJson( const std::vector<RESULT_ROW>& aRows, const wxString& aOutDir )
932{
933 wxFileName outFile( aOutDir, wxT( "results.json" ) );
934 std::ofstream out( outFile.GetFullPath().fn_str() );
935
936 if( !out.is_open() )
937 return;
938
939 out << "[\n";
940
941 for( size_t i = 0; i < aRows.size(); ++i )
942 {
943 const RESULT_ROW& row = aRows[i];
944
945 out << " {\n";
946 out << " \"board\": \"" << jsonEscape( row.board ) << "\",\n";
947 out << " \"config\": \"" << jsonEscape( row.config ) << "\",\n";
948 out << " \"compile_ms\": " << row.compile.median << ",\n";
949 out << " \"compile_mad\": " << row.compile.mad << ",\n";
950 out << " \"cache_gen_ms\": " << row.cacheGen.median << ",\n";
951 out << " \"cache_gen_mad\": " << row.cacheGen.mad << ",\n";
952 out << " \"check_ms\": " << row.check.median << ",\n";
953 out << " \"check_mad\": " << row.check.mad << ",\n";
954
955 if( row.evalOverheadValid )
956 out << " \"eval_overhead_ms\": " << row.evalOverheadMs << ",\n";
957 else
958 out << " \"eval_overhead_ms\": null,\n";
959
960 out << " \"n_violations\": " << row.violations << ",\n";
961 out << " \"under_load\": " << ( row.underLoad ? "true" : "false" ) << ",\n";
962 out << " \"timed_out\": " << ( row.timedOut ? "true" : "false" ) << ",\n";
963 out << " \"percent_complete\": " << ( row.timedOut ? row.fraction * 100.0 : 100.0 )
964 << ",\n";
965 out << " \"per_provider\": {";
966
967 bool first = true;
968
969 for( const auto& [name, stat] : row.perProvider )
970 {
971 out << ( first ? "\n" : ",\n" );
972 out << " \"" << jsonEscape( name ) << "\": { \"median_ms\": " << stat.median
973 << ", \"mad_ms\": " << stat.mad << " }";
974 first = false;
975 }
976
977 out << ( first ? "}" : "\n }" ) << "\n";
978 out << " }" << ( i + 1 < aRows.size() ? "," : "" ) << "\n";
979 }
980
981 out << "]\n";
982}
983
984
990void writeWorstOffenders( const std::vector<RESULT_ROW>& aRows, const wxString& aOutDir, int aTopN )
991{
992 std::vector<const RESULT_ROW*> byCompile;
993 std::vector<const RESULT_ROW*> byEval;
994 std::vector<const RESULT_ROW*> timedOut;
995
996 for( const RESULT_ROW& row : aRows )
997 {
998 byCompile.push_back( &row );
999
1000 if( row.evalOverheadValid )
1001 byEval.push_back( &row );
1002
1003 if( row.timedOut )
1004 timedOut.push_back( &row );
1005 }
1006
1007 std::sort( byCompile.begin(), byCompile.end(),
1008 []( const RESULT_ROW* a, const RESULT_ROW* b )
1009 {
1010 return a->compile.median > b->compile.median;
1011 } );
1012
1013 std::sort( byEval.begin(), byEval.end(),
1014 []( const RESULT_ROW* a, const RESULT_ROW* b )
1015 {
1016 return a->evalOverheadMs > b->evalOverheadMs;
1017 } );
1018
1019 // Least-complete first: a cell that only reached 10% before the deadline is a worse
1020 // offender than one that reached 90%.
1021 std::sort( timedOut.begin(), timedOut.end(),
1022 []( const RESULT_ROW* a, const RESULT_ROW* b )
1023 {
1024 return a->fraction < b->fraction;
1025 } );
1026
1027 auto emitList = [&]( const char* aLabel )
1028 {
1029 std::printf( "=== worst offenders by %s ===\n", aLabel );
1030 std::printf( "%-28s %-22s %12s\n", "board", "config", aLabel );
1031 std::printf( "%-28s %-22s %12s\n", "----------------------------",
1032 "----------------------", "------------" );
1033 };
1034
1035 emitList( "compile_ms" );
1036
1037 for( int i = 0; i < aTopN && i < static_cast<int>( byCompile.size() ); ++i )
1038 {
1039 const RESULT_ROW* row = byCompile[i];
1040
1041 std::printf( "%-28s %-22s %12.3f\n", static_cast<const char*>( row->board.utf8_str() ),
1042 static_cast<const char*>( row->config.utf8_str() ), row->compile.median );
1043 }
1044
1045 std::printf( "\n" );
1046 emitList( "eval_overhead_ms" );
1047
1048 for( int i = 0; i < aTopN && i < static_cast<int>( byEval.size() ); ++i )
1049 {
1050 const RESULT_ROW* row = byEval[i];
1051
1052 std::printf( "%-28s %-22s %12.3f\n", static_cast<const char*>( row->board.utf8_str() ),
1053 static_cast<const char*>( row->config.utf8_str() ), row->evalOverheadMs );
1054 }
1055
1056 std::printf( "\n" );
1057
1058 if( !timedOut.empty() )
1059 {
1060 std::printf( "=== timed out (eval unbounded; ranked least-complete first) ===\n" );
1061 std::printf( "%-28s %-22s %12s\n", "board", "config", "percent" );
1062 std::printf( "%-28s %-22s %12s\n", "----------------------------",
1063 "----------------------", "------------" );
1064
1065 for( const RESULT_ROW* row : timedOut )
1066 {
1067 std::printf( "%-28s %-22s %11.1f%%\n",
1068 static_cast<const char*>( row->board.utf8_str() ),
1069 static_cast<const char*>( row->config.utf8_str() ), row->fraction * 100.0 );
1070 }
1071
1072 std::printf( "\n" );
1073 }
1074
1075 wxFileName outFile( aOutDir, wxT( "worst_offenders.json" ) );
1076 std::ofstream out( outFile.GetFullPath().fn_str() );
1077
1078 if( !out.is_open() )
1079 return;
1080
1081 auto writeRanked = [&]( const char* aKey, const std::vector<const RESULT_ROW*>& aList,
1082 bool aUseEval )
1083 {
1084 out << " \"" << aKey << "\": [\n";
1085
1086 int count = std::min<int>( aTopN, static_cast<int>( aList.size() ) );
1087
1088 for( int i = 0; i < count; ++i )
1089 {
1090 const RESULT_ROW* row = aList[i];
1091 double value = aUseEval ? row->evalOverheadMs : row->compile.median;
1092
1093 out << " { \"board\": \"" << jsonEscape( row->board ) << "\", \"config\": \""
1094 << jsonEscape( row->config ) << "\", \"" << ( aUseEval ? "eval_overhead_ms"
1095 : "compile_ms" )
1096 << "\": " << value << " }" << ( i + 1 < count ? "," : "" ) << "\n";
1097 }
1098
1099 out << " ]";
1100 };
1101
1102 out << "{\n";
1103 writeRanked( "by_compile_ms", byCompile, false );
1104 out << ",\n";
1105 writeRanked( "by_eval_overhead_ms", byEval, true );
1106 out << ",\n";
1107
1108 out << " \"timed_out\": [\n";
1109
1110 for( size_t i = 0; i < timedOut.size(); ++i )
1111 {
1112 const RESULT_ROW* row = timedOut[i];
1113
1114 out << " { \"board\": \"" << jsonEscape( row->board ) << "\", \"config\": \""
1115 << jsonEscape( row->config ) << "\", \"percent_complete\": " << row->fraction * 100.0
1116 << " }" << ( i + 1 < timedOut.size() ? "," : "" ) << "\n";
1117 }
1118
1119 out << " ]\n}\n";
1120}
1121
1122
1124wxString configTag( CACHE_MODE aCache, RULES_VARIANT aVariant, int aThreads )
1125{
1126 return wxString::Format( wxT( "%s/%s/t%d" ), aCache == CACHE_MODE::COLD ? "cold" : "warm",
1127 variantName( aVariant ), aThreads );
1128}
1129
1130} // namespace
1131
1132
1133int main( int argc, char** argv )
1134{
1135 wxInitialize( argc, argv );
1136
1137 // Force the C locale so numeric parsing of the trace lines stays on a dot decimal
1138 // separator regardless of the environment.
1139 std::setlocale( LC_ALL, "C" );
1140
1141 // Line-buffer stdout so per-board progress streams to a redirected log during a long sweep
1142 // instead of materializing only at exit.
1143 std::setvbuf( stdout, nullptr, _IOLBF, 0 );
1144
1145 // The self-check exercises the trace parser alone and needs no engine, project, or
1146 // board, so handle it before any heavier initialization.
1147 for( int i = 1; i < argc; ++i )
1148 {
1149 if( std::string( argv[i] ) == "--selftest" )
1150 {
1151 int rv = RunTraceCaptureSelftest();
1152 wxUninitialize();
1153
1154 return rv;
1155 }
1156 }
1157
1158 SetPgm( &g_program );
1159 Pgm().InitPgm( true, true );
1160
1162 propMgr.Rebuild();
1163
1164 static const wxCmdLineEntryDesc cmdLineDesc[] = {
1165 { wxCMD_LINE_SWITCH, nullptr, "selftest", "run the trace-parser self-check and exit",
1166 wxCMD_LINE_VAL_NONE, wxCMD_LINE_PARAM_OPTIONAL },
1167 { wxCMD_LINE_OPTION, nullptr, "board", "ad-hoc board override (skips the corpus manifest)",
1168 wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL },
1169 { wxCMD_LINE_OPTION, "r", "rules", "default-variant design rules file (.kicad_dru)",
1170 wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL },
1171 { wxCMD_LINE_OPTION, nullptr, "heavy-rules", "heavy-variant design rules file (.kicad_dru)",
1172 wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL },
1173 { wxCMD_LINE_OPTION, nullptr, "rules-variant",
1174 "none|default|heavy (default: sweep all three)", wxCMD_LINE_VAL_STRING,
1175 wxCMD_LINE_PARAM_OPTIONAL },
1176 { wxCMD_LINE_SWITCH, nullptr, "rules-only",
1177 "time only InitEngine() compile in a loop, no checks", wxCMD_LINE_VAL_NONE,
1178 wxCMD_LINE_PARAM_OPTIONAL },
1179 { wxCMD_LINE_OPTION, nullptr, "threads", "worker threads, 0=all (default 0)",
1180 wxCMD_LINE_VAL_NUMBER, wxCMD_LINE_PARAM_OPTIONAL },
1181 { wxCMD_LINE_OPTION, nullptr, "repeat", "timed repeats after warm-up (default 5)",
1182 wxCMD_LINE_VAL_NUMBER, wxCMD_LINE_PARAM_OPTIONAL },
1183 { wxCMD_LINE_OPTION, nullptr, "cache", "cold|warm|both (default both)",
1184 wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL },
1185 { wxCMD_LINE_OPTION, nullptr, "isolate",
1186 "ignore every provider but this one to attribute its eval cost",
1187 wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL },
1188 { wxCMD_LINE_OPTION, nullptr, "top-n", "worst-offender list length (default 10)",
1189 wxCMD_LINE_VAL_NUMBER, wxCMD_LINE_PARAM_OPTIONAL },
1190 { wxCMD_LINE_OPTION, nullptr, "out", "output directory for JSON artifacts (default cwd)",
1191 wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL },
1192 { wxCMD_LINE_OPTION, nullptr, "max-load",
1193 "abort when 1-min loadavg exceeds this (default ncores*0.5)", wxCMD_LINE_VAL_STRING,
1194 wxCMD_LINE_PARAM_OPTIONAL },
1195 { wxCMD_LINE_OPTION, nullptr, "timeout",
1196 "per-test deadline in seconds; 0 disables (default 60)", wxCMD_LINE_VAL_NUMBER,
1197 wxCMD_LINE_PARAM_OPTIONAL },
1198 { wxCMD_LINE_SWITCH, nullptr, "quick",
1199 "fast iteration set: small/synthetic boards, warm none+heavy, repeat 3",
1200 wxCMD_LINE_VAL_NONE, wxCMD_LINE_PARAM_OPTIONAL },
1201 { wxCMD_LINE_OPTION, nullptr, "quick-max-mb",
1202 "in --quick, also keep real boards smaller than this many MB (default 0 = synthetic only)",
1203 wxCMD_LINE_VAL_NUMBER, wxCMD_LINE_PARAM_OPTIONAL },
1204 { wxCMD_LINE_PARAM, nullptr, nullptr, "board.kicad_pcb", wxCMD_LINE_VAL_STRING,
1205 wxCMD_LINE_PARAM_OPTIONAL },
1206 { wxCMD_LINE_NONE }
1207 };
1208
1209 wxCmdLineParser parser( argc, argv );
1210 parser.SetDesc( cmdLineDesc );
1211 parser.SetLogo( "qa_drc_benchmark: time DRC rule compile + expression evaluation" );
1212
1213 if( parser.Parse() != 0 )
1214 {
1215 Pgm().Destroy();
1216 wxUninitialize();
1217 return 1;
1218 }
1219
1221
1222 auto cleanupExit = [&]( int aCode )
1223 {
1224 Pgm().Destroy();
1225 wxUninitialize();
1226 return aCode;
1227 };
1228
1229 // Resolve which boards to run. An explicit --board or a positional board is an ad-hoc run;
1230 // otherwise the corpus manifest drives the sweep. An unconfigured corpus with no ad-hoc
1231 // board is a clean skip, never a failure.
1232 std::vector<CORPUS_ENTRY> entries;
1233
1234 wxString adHocBoard;
1235 bool haveAdHocBoard = parser.Found( "board", &adHocBoard );
1236
1237 if( !haveAdHocBoard && parser.GetParamCount() > 0 )
1238 {
1239 adHocBoard = parser.GetParam( 0 );
1240 haveAdHocBoard = true;
1241 }
1242
1243 std::optional<wxString> cliDefaultRules;
1244 wxString rulesArg;
1245
1246 if( parser.Found( "r", &rulesArg ) )
1247 cliDefaultRules = rulesArg;
1248
1249 std::optional<wxString> cliHeavyRules;
1250 wxString heavyArg;
1251
1252 if( parser.Found( "heavy-rules", &heavyArg ) )
1253 cliHeavyRules = heavyArg;
1254
1255 if( haveAdHocBoard )
1256 {
1257 CORPUS_ENTRY entry;
1258 wxFileName board( adHocBoard );
1259 board.MakeAbsolute();
1260 entry.board = board.GetFullPath();
1261
1262 if( cliDefaultRules )
1263 {
1264 wxFileName rules( *cliDefaultRules );
1265 rules.MakeAbsolute();
1266 entry.rules = rules.GetFullPath();
1267 }
1268
1269 entry.tier = wxT( "adhoc" );
1270 entries.push_back( entry );
1271 }
1272 else
1273 {
1274 if( !CORPUS::IsConfigured() )
1275 {
1276 std::printf( "KICAD_DRC_BENCH_CORPUS is unset or does not name a directory; "
1277 "nothing to benchmark.\n"
1278 "Set it to a corpus root containing corpus.json, or pass a board "
1279 "with --board <file.kicad_pcb>. Skipping.\n" );
1280 return cleanupExit( 0 );
1281 }
1282
1283 wxString loadError;
1284
1285 if( !CORPUS::Load( entries, loadError ) )
1286 {
1287 std::printf( "error loading corpus: %s\n",
1288 static_cast<const char*>( loadError.utf8_str() ) );
1289 return cleanupExit( 1 );
1290 }
1291
1292 if( entries.empty() )
1293 {
1294 std::printf( "corpus at %s has no entries; nothing to benchmark.\n",
1295 static_cast<const char*>( CORPUS::Root().utf8_str() ) );
1296 return cleanupExit( 0 );
1297 }
1298 }
1299
1300 long threadsArg = 0;
1301 int threads = 0;
1302
1303 if( parser.Found( "threads", &threadsArg ) )
1304 threads = static_cast<int>( threadsArg );
1305
1306 bool quick = parser.Found( "quick" );
1307
1308 double timeoutSec = 60.0;
1309 long timeoutArg = 60;
1310
1311 if( parser.Found( "timeout", &timeoutArg ) )
1312 timeoutSec = static_cast<double>( std::max( 0L, timeoutArg ) );
1313
1314 // Default 0 keeps the quick set to the curated synthetic stressors only; a positive value
1315 // opts small real boards back in for a slightly broader but slower iteration loop.
1316 double quickMaxMb = 0.0;
1317 long quickMaxArg = 0;
1318
1319 if( parser.Found( "quick-max-mb", &quickMaxArg ) )
1320 quickMaxMb = static_cast<double>( std::max( 0L, quickMaxArg ) );
1321
1322 long repeatArg = 5;
1323 int repeat = quick ? 3 : 5;
1324
1325 if( parser.Found( "repeat", &repeatArg ) )
1326 repeat = std::max( 1, static_cast<int>( repeatArg ) );
1327
1328 long topNArg = 10;
1329 int topN = 10;
1330
1331 if( parser.Found( "top-n", &topNArg ) )
1332 topN = std::max( 1, static_cast<int>( topNArg ) );
1333
1334 std::optional<wxString> isolate;
1335 wxString isolateArg;
1336
1337 if( parser.Found( "isolate", &isolateArg ) )
1338 {
1339 isolate = isolateArg;
1340
1341 if( !providerErrorCodes().count( isolateArg ) )
1342 std::printf( "warning: --isolate '%s' has no known error codes; the full provider "
1343 "set will run.\n",
1344 static_cast<const char*>( isolateArg.utf8_str() ) );
1345 }
1346
1347 wxString outDir = wxFileName::GetCwd();
1348 wxString outArg;
1349
1350 if( parser.Found( "out", &outArg ) )
1351 outDir = outArg;
1352
1353 unsigned ncores = std::max( 1u, std::thread::hardware_concurrency() );
1354 double maxLoad = ncores * 0.5;
1355 wxString maxLoadArg;
1356
1357 if( parser.Found( "max-load", &maxLoadArg ) )
1358 maxLoadArg.ToCDouble( &maxLoad );
1359
1360 // Low-load guard. The benchmark only means anything on an otherwise-idle machine, so refuse
1361 // to start under contention rather than emit noisy numbers the optimizer might trust.
1362 double startLoad = readOneMinuteLoad();
1363
1364 if( maxLoad > 0.0 && startLoad > maxLoad )
1365 {
1366 std::printf( "aborting: 1-min loadavg %.2f exceeds limit %.2f (cores=%u). "
1367 "Run on an idle machine or raise --max-load.\n",
1368 startLoad, maxLoad, ncores );
1369 return cleanupExit( 2 );
1370 }
1371
1372 std::vector<CACHE_MODE> cacheModes = { CACHE_MODE::COLD, CACHE_MODE::WARM };
1373 wxString cacheArg;
1374
1375 if( parser.Found( "cache", &cacheArg ) )
1376 {
1377 if( cacheArg == wxT( "cold" ) )
1378 cacheModes = { CACHE_MODE::COLD };
1379 else if( cacheArg == wxT( "warm" ) )
1380 cacheModes = { CACHE_MODE::WARM };
1381 else if( cacheArg != wxT( "both" ) )
1382 {
1383 std::printf( "error: --cache must be cold|warm|both\n" );
1384 return cleanupExit( 1 );
1385 }
1386 }
1387 else if( quick )
1388 {
1389 // Warm is the iteration-relevant number and halves the work versus cold+warm.
1390 cacheModes = { CACHE_MODE::WARM };
1391 }
1392
1393 std::vector<RULES_VARIANT> variants = { RULES_VARIANT::NONE, RULES_VARIANT::DEFAULT,
1394 RULES_VARIANT::HEAVY };
1395 wxString variantArg;
1396
1397 if( parser.Found( "rules-variant", &variantArg ) )
1398 {
1399 RULES_VARIANT v = RULES_VARIANT::DEFAULT;
1400
1401 if( !parseVariant( variantArg, v ) )
1402 {
1403 std::printf( "error: --rules-variant must be none|default|heavy\n" );
1404 return cleanupExit( 1 );
1405 }
1406
1407 variants = { v };
1408 }
1409 else if( quick )
1410 {
1411 // none gives the geometric baseline and heavy gives compile_ms plus the eval overhead
1412 // over that baseline; the default variant adds cost without new signal in quick mode.
1413 variants = { RULES_VARIANT::NONE, RULES_VARIANT::HEAVY };
1414 }
1415
1416 // In quick mode keep only the immediately meaningful boards: the curated fast cells that
1417 // expose compiler and evaluator cost in seconds. Honor explicit "quick" manifest flags when
1418 // present, otherwise fall back to the synthetic tier C. --quick-max-mb opts small real boards
1419 // back in. The multi-minute giants belong to the full sweep, not the iteration loop.
1420 if( quick && !haveAdHocBoard )
1421 {
1422 bool anyFlagged = std::any_of( entries.begin(), entries.end(),
1423 []( const CORPUS_ENTRY& e ) { return e.quick; } );
1424
1425 std::vector<CORPUS_ENTRY> kept;
1426
1427 for( const CORPUS_ENTRY& entry : entries )
1428 {
1429 wxULongLong size = wxFileName::GetSize( entry.board );
1430 bool small = quickMaxMb > 0.0 && size != wxInvalidSize
1431 && size.ToDouble() < quickMaxMb * 1.0e6;
1432 bool flagged = anyFlagged ? entry.quick : ( entry.tier == wxT( "C" ) );
1433
1434 if( flagged || small )
1435 kept.push_back( entry );
1436 }
1437
1438 entries.swap( kept );
1439 }
1440
1441 std::printf( "corpus: %s\n", haveAdHocBoard
1442 ? "ad-hoc"
1443 : static_cast<const char*>( CORPUS::Root().utf8_str() ) );
1444 std::printf( "boards: %zu%s\n", entries.size(), quick ? " (quick set)" : "" );
1445 std::printf( "threads: %d (0=all)\n", threads );
1446 std::printf( "repeat: %d\n", repeat );
1447 std::printf( "timeout: %.0f s/test%s\n", timeoutSec, timeoutSec > 0.0 ? "" : " (disabled)" );
1448 std::printf( "max-load: %.2f (start %.2f, cores %u)\n", maxLoad, startLoad, ncores );
1449
1450 if( isolate )
1451 std::printf( "isolate: %s\n", static_cast<const char*>( isolate->utf8_str() ) );
1452
1453 std::printf( "out: %s\n\n", static_cast<const char*>( outDir.utf8_str() ) );
1454
1455 int rv = 0;
1456 std::vector<COVERAGE_ROW> coverageRows;
1457 std::vector<RESULT_ROW> resultRows;
1458
1459 bool rulesOnly = parser.Found( "rules-only" );
1460
1461 for( const CORPUS_ENTRY& entry : entries )
1462 {
1463 wxFileName boardName( entry.board );
1464 wxFileName projectName( boardName );
1465 projectName.SetExt( FILEEXT::ProjectFileExtension );
1466
1467 // A single malformed corpus board must not take down the whole sweep, so isolate every
1468 // board's load + timing behind a catch that records the failure and moves on.
1469 try
1470 {
1471
1472 if( projectName.Exists() )
1473 manager.LoadProject( projectName.GetFullPath() );
1474
1475 // The manifest rules path feeds both the default and heavy variants; ad-hoc runs may
1476 // instead carry an explicit --rules. Heavy falls back to --heavy-rules when present.
1477 std::optional<wxString> entryDefaultRules;
1478
1479 if( !entry.rules.IsEmpty() )
1480 entryDefaultRules = entry.rules;
1481 else if( cliDefaultRules )
1482 entryDefaultRules = cliDefaultRules;
1483
1484 std::optional<wxString> entryHeavyRules = cliHeavyRules ? cliHeavyRules : entryDefaultRules;
1485
1486 std::printf( "### board: %s (tier %s)\n",
1487 static_cast<const char*>( boardName.GetFullName().utf8_str() ),
1488 static_cast<const char*>( entry.tier.utf8_str() ) );
1489
1490 // Coverage uses the default-variant rules so the matrix reflects the rule set the corpus
1491 // ships, independent of any heavy sibling used only for stress timing.
1492 wxFileName coverageRules =
1493 resolveRules( boardName, entryDefaultRules, entryHeavyRules, RULES_VARIANT::DEFAULT );
1494
1495 coverageRows.push_back(
1496 collectCoverage( boardName, manager, projectName, coverageRules ) );
1497
1498 if( rulesOnly )
1499 {
1500 std::printf( "%-10s %14s %12s\n", "variant", "compile_med", "compile_mad" );
1501 std::printf( "%-10s %14s %12s\n", "----------", "--------------", "------------" );
1502
1503 for( RULES_VARIANT variant : variants )
1504 {
1505 wxFileName rulesFile =
1506 resolveRules( boardName, entryDefaultRules, entryHeavyRules, variant );
1507
1508 STAT compile =
1509 runCompileOnly( boardName, manager, projectName, rulesFile, repeat );
1510
1511 std::printf( "%-10s %14.3f %12.3f\n", variantName( variant ), compile.median,
1512 compile.mad );
1513
1514 RESULT_ROW resultRow;
1515 resultRow.board = boardName.GetFullName();
1516 resultRow.config = configTag( CACHE_MODE::COLD, variant, threads ) + wxT( "/compile" );
1517 resultRow.compile = compile;
1518 resultRows.push_back( resultRow );
1519 }
1520
1521 std::printf( "\n" );
1522 continue;
1523 }
1524
1525 for( CACHE_MODE cache : cacheModes )
1526 {
1527 const char* cacheLabel = cache == CACHE_MODE::COLD ? "cold" : "warm";
1528
1529 std::printf( "--- cache: %s ---\n", cacheLabel );
1530 std::printf( "%-10s %12s %10s %12s %12s %12s %10s\n", "variant", "compile_ms", "(mad)",
1531 "cache_gen", "check_ms", "eval_ovhd", "violations" );
1532 std::printf( "%-10s %12s %10s %12s %12s %12s %10s\n", "----------", "------------",
1533 "----------", "------------", "------------", "------------",
1534 "----------" );
1535
1536 std::optional<double> noneCheck;
1537
1538 for( RULES_VARIANT variant : variants )
1539 {
1540 BENCH_CONFIG config;
1541 config.rulesVariant = variant;
1542 config.cache = cache;
1543 config.threads = threads;
1544 config.repeat = repeat;
1545
1546 wxFileName rulesFile =
1547 resolveRules( boardName, entryDefaultRules, entryHeavyRules, variant );
1548
1549 SWEEP_RESULT result = runConfig( boardName, manager, projectName, rulesFile, config,
1550 isolate, maxLoad, timeoutSec );
1551
1552 if( !result.ran )
1553 {
1554 rv = 1;
1555 continue;
1556 }
1557
1558 // A timed-out none baseline cannot anchor eval_overhead, so only record it when
1559 // the check actually completed.
1560 if( variant == RULES_VARIANT::NONE && !result.timedOut )
1561 noneCheck = result.check.median;
1562
1563 RESULT_ROW resultRow;
1564 resultRow.board = boardName.GetFullName();
1565 resultRow.config = configTag( cache, variant, threads );
1566 resultRow.compile = result.compile;
1567 resultRow.cacheGen = result.cacheGen;
1568 resultRow.check = result.check;
1569 resultRow.violations = result.violations;
1570 resultRow.underLoad = result.underLoad;
1571 resultRow.timedOut = result.timedOut;
1572 resultRow.fraction = result.fraction;
1573 resultRow.perProvider = result.providerStats;
1574
1575 // eval_overhead isolates what the evaluator adds over the rules-free geometric
1576 // baseline for the same board and cache mode. It only has meaning when both this
1577 // check and the none baseline completed in this same sweep; a timeout leaves the
1578 // check time partial, so the overhead stays unrecorded.
1579 wxString ovhd = wxT( "n/a" );
1580
1581 if( result.timedOut )
1582 {
1583 ovhd = wxT( "timeout" );
1584 }
1585 else if( variant == RULES_VARIANT::NONE )
1586 {
1587 resultRow.evalOverheadMs = 0.0;
1588 resultRow.evalOverheadValid = true;
1589 ovhd = wxT( "0.000" );
1590 }
1591 else if( noneCheck )
1592 {
1593 resultRow.evalOverheadMs = result.check.median - *noneCheck;
1594 resultRow.evalOverheadValid = true;
1595 ovhd = wxString::Format( wxT( "%.3f" ), resultRow.evalOverheadMs );
1596 }
1597
1598 resultRows.push_back( resultRow );
1599
1600 if( result.timedOut )
1601 {
1602 std::printf( "%-10s %12.3f %10.3f %12s %12s %12s [TIMEOUT %.0f%%]\n",
1603 variantName( variant ), result.compile.median, result.compile.mad,
1604 "-", "-", static_cast<const char*>( ovhd.utf8_str() ),
1605 result.fraction * 100.0 );
1606 }
1607 else
1608 {
1609 std::printf( "%-10s %12.3f %10.3f %12.3f %12.3f %12s %10d%s\n",
1610 variantName( variant ), result.compile.median, result.compile.mad,
1611 result.cacheGen.median, result.check.median,
1612 static_cast<const char*>( ovhd.utf8_str() ), result.violations,
1613 result.underLoad ? " [UNDER_LOAD]" : "" );
1614 }
1615 }
1616
1617 std::printf( "\n" );
1618 }
1619
1620 }
1621 catch( const std::exception& e )
1622 {
1623 std::printf( "error: board '%s' failed and was skipped: %s\n\n",
1624 static_cast<const char*>( boardName.GetFullName().utf8_str() ), e.what() );
1625 rv = 1;
1626 }
1627 }
1628
1629 emitCoverage( coverageRows, outDir );
1630 writeWorstOffenders( resultRows, outDir, topN );
1631 writeResultsJson( resultRows, outDir );
1632
1633 std::printf( "wrote results.json, worst_offenders.json, coverage.json to %s\n",
1634 static_cast<const char*>( outDir.utf8_str() ) );
1635
1636 return cleanupExit( rv );
1637}
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:373
BOARD_DESIGN_SETTINGS & GetDesignSettings() const
Definition board.cpp:1158
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:184
BS::priority_thread_pool & GetThreadPool()
Definition pgm_base.cpp:145
bool InitPgm(bool aHeadless=false, bool aIsUnitTest=false)
Initialize this program.
Definition pgm_base.cpp:343
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_DP_UNCOUPLED_LENGTH_TOO_LONG
Definition drc_item.h:109
@ DRCE_SKEW_OUT_OF_RANGE
Definition drc_item.h:106
@ DRCE_CREEPAGE
Definition drc_item.h:42
@ DRCE_HOLE_CLEARANCE
Definition drc_item.h:52
@ DRCE_SILK_EDGE_CLEARANCE
Definition drc_item.h:97
@ DRCE_SILK_MASK_CLEARANCE
Definition drc_item.h:95
@ DRCE_VIA_DIAMETER
Definition drc_item.h:59
@ DRCE_UNCONNECTED_ITEMS
Definition drc_item.h:37
@ DRCE_DP_GAP_OUT_OF_RANGE
Definition drc_item.h:108
@ DRCE_TRACK_WIDTH
Definition drc_item.h:53
@ DRCE_PADSTACK
Definition drc_item.h:60
@ DRCE_MIRRORED_TEXT_ON_FRONT_LAYER
Definition drc_item.h:111
@ DRCE_OVERLAPPING_FOOTPRINTS
Definition drc_item.h:63
@ DRCE_TRACK_ON_POST_MACHINED_LAYER
Definition drc_item.h:117
@ DRCE_TEXT_ON_EDGECUTS
Definition drc_item.h:40
@ DRCE_NET_CHAIN_STUB_TOO_LONG
Definition drc_item.h:103
@ DRCE_DRILL_OUT_OF_RANGE
Definition drc_item.h:58
@ DRCE_EDGE_CLEARANCE
Definition drc_item.h:44
@ DRCE_NET_CHAIN_RETURN_PATH_BREAK
Definition drc_item.h:104
@ DRCE_STARVED_THERMAL
Definition drc_item.h:47
@ DRCE_TRACK_SEGMENT_LENGTH
Definition drc_item.h:55
@ DRCE_MISSING_COURTYARD
Definition drc_item.h:64
@ DRCE_TRACK_ANGLE
Definition drc_item.h:54
@ DRCE_TRACK_NOT_CENTERED_ON_VIA
Definition drc_item.h:119
@ DRCE_CLEARANCE
Definition drc_item.h:41
@ DRCE_ISOLATED_COPPER
Definition drc_item.h:46
@ DRCE_DRILLED_HOLES_TOO_CLOSE
Definition drc_item.h:50
@ DRCE_ALLOWED_ITEMS
Definition drc_item.h:39
@ DRCE_COPPER_SLIVER
Definition drc_item.h:91
@ DRCE_PTH_IN_COURTYARD
Definition drc_item.h:67
@ DRCE_MICROVIA_DRILL_OUT_OF_RANGE
Definition drc_item.h:62
@ DRCE_SHORTING_ITEMS
Definition drc_item.h:38
@ DRCE_MALFORMED_COURTYARD
Definition drc_item.h:65
@ DRCE_FIRST
Definition drc_item.h:36
@ DRCE_DANGLING_VIA
Definition drc_item.h:48
@ DRCE_FOOTPRINT_TYPE_MISMATCH
Definition drc_item.h:79
@ DRCE_DANGLING_TRACK
Definition drc_item.h:49
@ DRCE_TEXT_HEIGHT
Definition drc_item.h:99
@ DRCE_SOLDERMASK_BRIDGE
Definition drc_item.h:92
@ DRCE_DRILLED_HOLES_COLOCATED
Definition drc_item.h:51
@ DRCE_SILK_CLEARANCE
Definition drc_item.h:98
@ DRCE_LENGTH_OUT_OF_RANGE
Definition drc_item.h:102
@ DRCE_LAST
Definition drc_item.h:123
@ DRCE_PAD_TH_WITH_NO_HOLE
Definition drc_item.h:82
@ DRCE_UNMIRRORED_TEXT_ON_BACK_LAYER
Definition drc_item.h:112
@ DRCE_TEXT_THICKNESS
Definition drc_item.h:100
@ DRCE_NPTH_IN_COURTYARD
Definition drc_item.h:68
@ DRCE_CONNECTION_WIDTH
Definition drc_item.h:57
@ DRCE_TRACKS_CROSSING
Definition drc_item.h:43
@ DRCE_VIA_COUNT_OUT_OF_RANGE
Definition drc_item.h:107
@ DRCE_ANNULAR_WIDTH
Definition drc_item.h:56
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.