KiCad PCB EDA Suite
Loading...
Searching...
No Matches
bench_spatial_index.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 3
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
21
22#include <geometry/rtree.h>
26
27#include <core/profile.h>
28#include <nlohmann/json.hpp>
29
30#include <filesystem>
31#include <fstream>
32#include <random>
33#include <vector>
34
35
36namespace
37{
38
39struct BBOX
40{
41 int min[2];
42 int max[2];
43};
44
45
46std::vector<BBOX> generateRandomBoxes( int aCount, int aMaxCoord, int aMaxSize,
47 std::mt19937& aRng )
48{
49 std::uniform_int_distribution<int> coordDist( 0, aMaxCoord );
50 std::uniform_int_distribution<int> sizeDist( 1, aMaxSize );
51
52 std::vector<BBOX> boxes( aCount );
53
54 for( int i = 0; i < aCount; ++i )
55 {
56 boxes[i].min[0] = coordDist( aRng );
57 boxes[i].min[1] = coordDist( aRng );
58 boxes[i].max[0] = boxes[i].min[0] + sizeDist( aRng );
59 boxes[i].max[1] = boxes[i].min[1] + sizeDist( aRng );
60 }
61
62 return boxes;
63}
64
65
66} // anonymous namespace
67
68
70{
71 std::string workload;
72 std::string implementation;
73 int itemCount = 0;
74 double buildMs = 0.0;
75 double queryMs = 0.0;
76 int queryCount = 0;
77 double queriesPerSec = 0.0;
78 size_t memoryBytes = 0;
79};
80
81
82BOOST_AUTO_TEST_SUITE( SpatialIndexBenchmark )
83
84
86{
87 // DRC pattern: bulk insert, then many range queries
88 const std::vector<int> scales = { 1000, 10000, 100000 };
89 std::vector<BENCH_RESULT> results;
90
91 for( int N : scales )
92 {
93 std::mt19937 rng( 42 );
94 auto boxes = generateRandomBoxes( N, 1000000, 10000, rng );
95
96 // Generate query boxes (viewport-sized regions)
97 auto queries = generateRandomBoxes( 1000, 1000000, 50000, rng );
98
99 // --- Old RTree ---
100 {
102 result.workload = "DRC";
103 result.implementation = "OldTree";
104 result.itemCount = N;
105
106 PROF_TIMER buildTimer;
107 buildTimer.Start();
108
109 RTree<intptr_t, int, 2, double, 8, 4> oldTree;
110
111 for( int i = 0; i < N; ++i )
112 oldTree.Insert( boxes[i].min, boxes[i].max, static_cast<intptr_t>( i ) );
113
114 result.buildMs = buildTimer.msecs();
115
116 PROF_TIMER queryTimer;
117 queryTimer.Start();
118 int totalFound = 0;
119
120 for( const BBOX& q : queries )
121 {
122 auto visitor = [&totalFound]( intptr_t )
123 {
124 totalFound++;
125 return true;
126 };
127
128 oldTree.Search( q.min, q.max, visitor );
129 }
130
131 result.queryMs = queryTimer.msecs();
132 result.queryCount = static_cast<int>( queries.size() );
133 result.queriesPerSec = result.queryMs > 0
134 ? ( queries.size() / ( result.queryMs / 1e3 ) )
135 : 0;
136
137 results.push_back( result );
138 }
139
140 // --- Packed RTree ---
141 {
143 result.workload = "DRC";
144 result.implementation = "PackedTree";
145 result.itemCount = N;
146
147 PROF_TIMER buildTimer;
148 buildTimer.Start();
149
151 builder.Reserve( N );
152
153 for( int i = 0; i < N; ++i )
154 builder.Add( boxes[i].min, boxes[i].max, static_cast<intptr_t>( i ) );
155
156 auto packedTree = builder.Build();
157 result.buildMs = buildTimer.msecs();
158 result.memoryBytes = packedTree.MemoryUsage();
159
160 PROF_TIMER queryTimer;
161 queryTimer.Start();
162 int totalFound = 0;
163
164 for( const BBOX& q : queries )
165 {
166 auto visitor = [&totalFound]( intptr_t )
167 {
168 totalFound++;
169 return true;
170 };
171
172 packedTree.Search( q.min, q.max, visitor );
173 }
174
175 result.queryMs = queryTimer.msecs();
176 result.queryCount = static_cast<int>( queries.size() );
177 result.queriesPerSec = result.queryMs > 0
178 ? ( queries.size() / ( result.queryMs / 1e3 ) )
179 : 0;
180
181 results.push_back( result );
182 }
183
184 // --- Dynamic RTree ---
185 {
187 result.workload = "DRC";
188 result.implementation = "DynTree";
189 result.itemCount = N;
190
191 PROF_TIMER buildTimer;
192 buildTimer.Start();
193
195
196 for( int i = 0; i < N; ++i )
197 dynTree.Insert( boxes[i].min, boxes[i].max, static_cast<intptr_t>( i ) );
198
199 result.buildMs = buildTimer.msecs();
200 result.memoryBytes = dynTree.MemoryUsage();
201
202 PROF_TIMER queryTimer;
203 queryTimer.Start();
204 int totalFound = 0;
205
206 for( const BBOX& q : queries )
207 {
208 auto visitor = [&totalFound]( intptr_t )
209 {
210 totalFound++;
211 return true;
212 };
213
214 dynTree.Search( q.min, q.max, visitor );
215 }
216
217 result.queryMs = queryTimer.msecs();
218 result.queryCount = static_cast<int>( queries.size() );
219 result.queriesPerSec = result.queryMs > 0
220 ? ( queries.size() / ( result.queryMs / 1e3 ) )
221 : 0;
222
223 results.push_back( result );
224 }
225 }
226
227 // Output JSON results
228 nlohmann::json j;
229
230 for( const BENCH_RESULT& r : results )
231 {
232 nlohmann::json entry;
233 entry["workload"] = r.workload;
234 entry["implementation"] = r.implementation;
235 entry["itemCount"] = r.itemCount;
236 entry["buildMs"] = r.buildMs;
237 entry["queryMs"] = r.queryMs;
238 entry["queryCount"] = r.queryCount;
239 entry["queriesPerSec"] = r.queriesPerSec;
240 entry["memoryBytes"] = r.memoryBytes;
241 j.push_back( entry );
242 }
243
244 // Write to file if output directory exists
245 std::filesystem::path outputDir( std::filesystem::temp_directory_path()
246 / "kicad_bench" );
247 std::filesystem::create_directories( outputDir );
248 std::ofstream out( outputDir / "spatial_index_drc.json" );
249
250 if( out.is_open() )
251 out << j.dump( 2 ) << std::endl;
252
253 // Regression check: new trees must be faster than old tree for queries at N>=10K
254 for( size_t i = 0; i + 2 < results.size(); i += 3 )
255 {
256 if( results[i].itemCount >= 10000 )
257 {
258 double oldQps = results[i].queriesPerSec;
259 double packedQps = results[i + 1].queriesPerSec;
260 double dynQps = results[i + 2].queriesPerSec;
261
262 BOOST_CHECK_GE( packedQps, oldQps );
263 BOOST_CHECK_GE( dynQps, oldQps );
264 }
265 }
266}
267
268
269BOOST_AUTO_TEST_CASE( RouterWorkload )
270{
271 // Router pattern: interleaved insert/remove/query (10%/10%/80%)
272 const std::vector<int> scales = { 100, 1000, 5000 };
273
274 for( int N : scales )
275 {
276 // --- Old RTree baseline ---
277 {
278 std::mt19937 rng( 123 );
279 auto boxes = generateRandomBoxes( N, 100000, 5000, rng );
280
281 RTree<intptr_t, int, 2, double, 8, 4> oldTree;
282
283 for( int i = 0; i < N; ++i )
284 oldTree.Insert( boxes[i].min, boxes[i].max, static_cast<intptr_t>( i ) );
285
286 std::uniform_int_distribution<int> opDist( 0, 9 );
287 std::uniform_int_distribution<int> coordDist( 0, 100000 );
288 std::uniform_int_distribution<int> sizeDist( 1, 5000 );
289
290 int ops = N * 10;
291
292 PROF_TIMER timer;
293 timer.Start();
294
295 for( int op = 0; op < ops; ++op )
296 {
297 int which = opDist( rng );
298
299 if( which == 0 )
300 {
301 int min[2] = { coordDist( rng ), coordDist( rng ) };
302 int max[2] = { min[0] + sizeDist( rng ), min[1] + sizeDist( rng ) };
303 oldTree.Insert( min, max, static_cast<intptr_t>( N + op ) );
304 }
305 else if( which == 1 && !boxes.empty() )
306 {
307 std::uniform_int_distribution<int> idxDist( 0, boxes.size() - 1 );
308 int idx = idxDist( rng );
309
310 oldTree.Remove( boxes[idx].min, boxes[idx].max,
311 static_cast<intptr_t>( idx ) );
312 }
313 else
314 {
315 int min[2] = { coordDist( rng ), coordDist( rng ) };
316 int max[2] = { min[0] + sizeDist( rng ) * 5,
317 min[1] + sizeDist( rng ) * 5 };
318 int found = 0;
319
320 auto visitor = [&found]( intptr_t )
321 {
322 found++;
323 return true;
324 };
325
326 oldTree.Search( min, max, visitor );
327 }
328 }
329
330 double oldMs = timer.msecs();
331
332 BOOST_TEST_MESSAGE( "Router OldTree N=" << N << ": " << ops
333 << " ops in " << oldMs << "ms" );
334 }
335
336 // --- Dynamic RTree ---
337 {
338 std::mt19937 rng( 123 );
339 auto boxes = generateRandomBoxes( N, 100000, 5000, rng );
340
342
343 for( int i = 0; i < N; ++i )
344 dynTree.Insert( boxes[i].min, boxes[i].max, static_cast<intptr_t>( i ) );
345
346 std::uniform_int_distribution<int> opDist( 0, 9 );
347 std::uniform_int_distribution<int> coordDist( 0, 100000 );
348 std::uniform_int_distribution<int> sizeDist( 1, 5000 );
349
350 int ops = N * 10;
351 int insertCount = 0;
352 int removeCount = 0;
353 int queryCount = 0;
354
355 PROF_TIMER timer;
356 timer.Start();
357
358 for( int op = 0; op < ops; ++op )
359 {
360 int which = opDist( rng );
361
362 if( which == 0 )
363 {
364 int min[2] = { coordDist( rng ), coordDist( rng ) };
365 int max[2] = { min[0] + sizeDist( rng ), min[1] + sizeDist( rng ) };
366 dynTree.Insert( min, max, static_cast<intptr_t>( N + insertCount ) );
367 insertCount++;
368 }
369 else if( which == 1 && !boxes.empty() )
370 {
371 std::uniform_int_distribution<int> idxDist( 0, boxes.size() - 1 );
372 int idx = idxDist( rng );
373
374 dynTree.Remove( boxes[idx].min, boxes[idx].max,
375 static_cast<intptr_t>( idx ) );
376 removeCount++;
377 }
378 else
379 {
380 int min[2] = { coordDist( rng ), coordDist( rng ) };
381 int max[2] = { min[0] + sizeDist( rng ) * 5,
382 min[1] + sizeDist( rng ) * 5 };
383 int found = 0;
384
385 auto visitor = [&found]( intptr_t )
386 {
387 found++;
388 return true;
389 };
390
391 dynTree.Search( min, max, visitor );
392 queryCount++;
393 }
394 }
395
396 double elapsedMs = timer.msecs();
397
398 BOOST_TEST_MESSAGE( "Router DynTree N=" << N << ": " << ops << " ops in "
399 << elapsedMs << "ms ("
400 << insertCount << " ins, "
401 << removeCount << " rem, "
402 << queryCount << " qry)" );
403 }
404 }
405}
406
407
408BOOST_AUTO_TEST_CASE( ViewportWorkload )
409{
410 // Viewport pattern: bulk insert, then rapid viewport-sized queries
411 // Simulates GAL rendering where the full board is loaded then the view pans/zooms
412 const std::vector<int> scales = { 1000, 10000, 100000 };
413 std::vector<BENCH_RESULT> results;
414
415 for( int N : scales )
416 {
417 std::mt19937 rng( 77 );
418
419 // Board items: spread across a 400mm x 300mm board in nanometers
420 auto boxes = generateRandomBoxes( N, 400000000, 2000000, rng );
421
422 // Viewport queries: typical screen-sized regions (~50mm x 40mm window)
423 auto queries = generateRandomBoxes( 2000, 400000000, 50000000, rng );
424
425 // --- Old RTree ---
426 {
428 result.workload = "Viewport";
429 result.implementation = "OldTree";
430 result.itemCount = N;
431
432 RTree<intptr_t, int, 2, double, 8, 4> oldTree;
433
434 PROF_TIMER buildTimer;
435 buildTimer.Start();
436
437 for( int i = 0; i < N; ++i )
438 oldTree.Insert( boxes[i].min, boxes[i].max, static_cast<intptr_t>( i ) );
439
440 result.buildMs = buildTimer.msecs();
441
442 PROF_TIMER queryTimer;
443 queryTimer.Start();
444
445 for( const BBOX& q : queries )
446 {
447 auto visitor = []( intptr_t ) { return true; };
448 oldTree.Search( q.min, q.max, visitor );
449 }
450
451 result.queryMs = queryTimer.msecs();
452 result.queryCount = static_cast<int>( queries.size() );
453 result.queriesPerSec = result.queryMs > 0
454 ? ( queries.size() / ( result.queryMs / 1e3 ) )
455 : 0;
456
457 results.push_back( result );
458 }
459
460 // --- Dynamic RTree ---
461 {
463 result.workload = "Viewport";
464 result.implementation = "DynTree";
465 result.itemCount = N;
466
468
469 PROF_TIMER buildTimer;
470 buildTimer.Start();
471
472 for( int i = 0; i < N; ++i )
473 dynTree.Insert( boxes[i].min, boxes[i].max, static_cast<intptr_t>( i ) );
474
475 result.buildMs = buildTimer.msecs();
476 result.memoryBytes = dynTree.MemoryUsage();
477
478 PROF_TIMER queryTimer;
479 queryTimer.Start();
480
481 for( const BBOX& q : queries )
482 {
483 auto visitor = []( intptr_t ) { return true; };
484 dynTree.Search( q.min, q.max, visitor );
485 }
486
487 result.queryMs = queryTimer.msecs();
488 result.queryCount = static_cast<int>( queries.size() );
489 result.queriesPerSec = result.queryMs > 0
490 ? ( queries.size() / ( result.queryMs / 1e3 ) )
491 : 0;
492
493 results.push_back( result );
494 }
495 }
496
497 // Output JSON results
498 nlohmann::json j;
499
500 for( const BENCH_RESULT& r : results )
501 {
502 nlohmann::json entry;
503 entry["workload"] = r.workload;
504 entry["implementation"] = r.implementation;
505 entry["itemCount"] = r.itemCount;
506 entry["buildMs"] = r.buildMs;
507 entry["queryMs"] = r.queryMs;
508 entry["queryCount"] = r.queryCount;
509 entry["queriesPerSec"] = r.queriesPerSec;
510 entry["memoryBytes"] = r.memoryBytes;
511 j.push_back( entry );
512 }
513
514 std::filesystem::path outputDir( std::filesystem::temp_directory_path() / "kicad_bench" );
515 std::filesystem::create_directories( outputDir );
516 std::ofstream out( outputDir / "spatial_index_viewport.json" );
517
518 if( out.is_open() )
519 out << j.dump( 2 ) << std::endl;
520
521 // Regression check: DynTree must be faster than OldTree for queries at N>=10K
522 for( size_t i = 0; i + 1 < results.size(); i += 2 )
523 {
524 if( results[i].itemCount >= 10000 )
525 {
526 double oldQps = results[i].queriesPerSec;
527 double dynQps = results[i + 1].queriesPerSec;
528
529 BOOST_CHECK_GE( dynQps, oldQps );
530 }
531 }
532}
533
534
535BOOST_AUTO_TEST_CASE( RouterBranchWorkload )
536{
537 // Simulates the actual PNS NODE::Branch() pattern: build a tree, then clone it.
538 // Compares O(N) item-by-item copy (old pattern) vs O(1) CoW clone (new pattern).
539 const std::vector<int> scales = { 1000, 5000, 15000 };
540
541 for( int N : scales )
542 {
543 std::mt19937 rng( 555 );
544 auto boxes = generateRandomBoxes( N, 100000, 5000, rng );
545
546 // Old pattern: build tree, then copy all items into a new tree
547 {
548 RTree<intptr_t, int, 2, double, 8, 4> srcTree;
549
550 for( int i = 0; i < N; ++i )
551 srcTree.Insert( boxes[i].min, boxes[i].max, static_cast<intptr_t>( i ) );
552
553 PROF_TIMER timer;
554 timer.Start();
555
556 RTree<intptr_t, int, 2, double, 8, 4> dstTree;
557
558 auto copyVisitor = [&dstTree, &boxes]( intptr_t val )
559 {
560 int idx = static_cast<int>( val );
561
562 dstTree.Insert( boxes[idx].min, boxes[idx].max, val );
563 return true;
564 };
565
566 int allMin[2] = { INT_MIN, INT_MIN };
567 int allMax[2] = { INT_MAX, INT_MAX };
568 srcTree.Search( allMin, allMax, copyVisitor );
569
570 double oldCopyMs = timer.msecs();
571
572 BOOST_TEST_MESSAGE( "Branch OldTree N=" << N << ": copy " << oldCopyMs << "ms" );
573 }
574
575 // New pattern: build tree, then O(1) CoW clone
576 {
578
579 for( int i = 0; i < N; ++i )
580 srcTree.Insert( boxes[i].min, boxes[i].max, static_cast<intptr_t>( i ) );
581
582 PROF_TIMER timer;
583 timer.Start();
584
585 auto clone = srcTree.Clone();
586
587 double cloneMs = timer.msecs();
588
589 BOOST_TEST_MESSAGE( "Branch CoW N=" << N << ": clone " << cloneMs << "ms" );
590
591 // CoW clone must be faster than old copy
592 BOOST_CHECK_EQUAL( clone.size(), static_cast<size_t>( N ) );
593 }
594 }
595}
596
597
599{
600 // CoW pattern: clone, divergent mutations, queries
601 const std::vector<int> scales = { 1000, 5000 };
602
603 for( int N : scales )
604 {
605 std::mt19937 rng( 456 );
606 auto boxes = generateRandomBoxes( N, 100000, 5000, rng );
607
609
610 for( int i = 0; i < N; ++i )
611 parent.Insert( boxes[i].min, boxes[i].max, static_cast<intptr_t>( i ) );
612
613 // Clone and mutate
614 PROF_TIMER timer;
615 timer.Start();
616
617 auto clone = parent.Clone();
618 double cloneMs = timer.msecs();
619
620 // Insert 100 items into clone
621 std::uniform_int_distribution<int> coordDist( 0, 100000 );
622 std::uniform_int_distribution<int> sizeDist( 1, 5000 );
623
624 for( int i = 0; i < 100; ++i )
625 {
626 int min[2] = { coordDist( rng ), coordDist( rng ) };
627 int max[2] = { min[0] + sizeDist( rng ), min[1] + sizeDist( rng ) };
628 clone.Insert( min, max, static_cast<intptr_t>( N + i ) );
629 }
630
631 // Run queries on both
632 auto queries = generateRandomBoxes( 500, 100000, 20000, rng );
633
634 PROF_TIMER queryTimer;
635 queryTimer.Start();
636
637 for( const BBOX& q : queries )
638 {
639 int found = 0;
640
641 auto visitor = [&found]( intptr_t )
642 {
643 found++;
644 return true;
645 };
646
647 clone.Search( q.min, q.max, visitor );
648 }
649
650 double queryMs = queryTimer.msecs();
651
652 BOOST_TEST_MESSAGE( "CoW N=" << N
653 << ": clone=" << cloneMs << "ms"
654 << ", 500 queries=" << queryMs << "ms"
655 << ", parent size=" << parent.size()
656 << ", clone size=" << clone.size() );
657
658 // Verify parent unmodified
659 BOOST_CHECK_EQUAL( parent.size(), static_cast<size_t>( N ) );
660 BOOST_CHECK_EQUAL( clone.size(), static_cast<size_t>( N + 100 ) );
661 }
662}
663
664
665BOOST_AUTO_TEST_CASE( CowDepthWorkload )
666{
667 // Chain of clones at various depths
668 const int N = 1000;
669 std::mt19937 rng( 789 );
670 auto boxes = generateRandomBoxes( N, 100000, 5000, rng );
671
673
674 for( int i = 0; i < N; ++i )
675 root.Insert( boxes[i].min, boxes[i].max, static_cast<intptr_t>( i ) );
676
677 const std::vector<int> depths = { 1, 2, 3, 5, 10 };
678
679 for( int depth : depths )
680 {
681 PROF_TIMER timer;
682 timer.Start();
683
684 std::vector<KIRTREE::COW_RTREE<intptr_t, int, 2>> chain;
685 chain.push_back( root.Clone() );
686
687 for( int d = 1; d < depth; ++d )
688 chain.push_back( chain.back().Clone() );
689
690 double cloneMs = timer.msecs();
691
692 // Query the deepest clone
693 auto queries = generateRandomBoxes( 200, 100000, 20000, rng );
694 int totalFound = 0;
695
696 PROF_TIMER queryTimer;
697 queryTimer.Start();
698
699 for( const BBOX& q : queries )
700 {
701 auto visitor = [&totalFound]( intptr_t )
702 {
703 totalFound++;
704 return true;
705 };
706
707 chain.back().Search( q.min, q.max, visitor );
708 }
709
710 double queryMs = queryTimer.msecs();
711
712 BOOST_TEST_MESSAGE( "CoW depth=" << depth
713 << ": clone chain=" << cloneMs << "ms"
714 << ", 200 queries=" << queryMs << "ms"
715 << ", found=" << totalFound );
716 }
717}
718
719
BOOST_AUTO_TEST_CASE(DRCWorkload)
Copy-on-Write wrapper for DYNAMIC_RTREE.
void Insert(const ELEMTYPE aMin[NUMDIMS], const ELEMTYPE aMax[NUMDIMS], const DATATYPE &aData)
Insert an item.
COW_RTREE Clone() const
Create a clone that shares the tree structure with this tree.
Dynamic R*-tree with SoA node layout and stored insertion bounding boxes.
int Search(const ELEMTYPE aMin[NUMDIMS], const ELEMTYPE aMax[NUMDIMS], VISITOR &aVisitor) const
Search for items whose bounding boxes overlap the query rectangle.
size_t MemoryUsage() const
Return approximate memory usage in bytes.
bool Remove(const ELEMTYPE aMin[NUMDIMS], const ELEMTYPE aMax[NUMDIMS], const DATATYPE &aData)
Remove an item using its stored insertion bounding box.
void Insert(const ELEMTYPE aMin[NUMDIMS], const ELEMTYPE aMax[NUMDIMS], const DATATYPE &aData)
Insert an item with the given bounding box.
Builder for constructing a PACKED_RTREE from a set of items.
void Add(const ELEMTYPE aMin[NUMDIMS], const ELEMTYPE aMax[NUMDIMS], const DATATYPE &aData)
void Reserve(size_t aCount)
A small class to help profiling.
Definition profile.h:46
void Start()
Start or restart the counter.
Definition profile.h:74
double msecs(bool aSinceLast=false)
Definition profile.h:147
static thread_local boost::mt19937 rng
Definition kiid.cpp:48
std::string implementation
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_AUTO_TEST_SUITE_END()
BOOST_TEST_MESSAGE("\n=== Real-World Polygon PIP Benchmark ===\n"<< formatTable(table))
const SHAPE_LINE_CHAIN chain
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")