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