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