1 // Copyright Ferdinand Majerech 2014. 2 // Distributed under the Boost Software License, Version 1.0. 3 // (See accompanying file LICENSE_1_0.txt or copy at 4 // http://www.boost.org/LICENSE_1_0.txt) 5 6 7 /** A 'chunky' event list that supports real-time adding of profiling data and related 8 * ranges/generators. */ 9 module tharsis.prof.chunkyeventlist; 10 11 12 13 import std.algorithm; 14 import std.array; 15 import std.stdio; 16 17 import tharsis.prof.event; 18 import tharsis.prof.profiler; 19 import tharsis.prof.ranges; 20 21 22 /** A list of events providing range 'slices', using chunks of profiling data for storage. 23 * 24 * Useful for real-time profiling (used by Despiker); can add new chunks of profile data 25 * in real time and create ranges to generate events in specified time or chunk slices 26 * without processing the preceding chunks. 27 */ 28 struct ChunkyEventList 29 { 30 /** A single chunk of profiling data. 31 * 32 * Public so the user can allocate chunks for ChunkyEventList storage. 33 */ 34 struct Chunk 35 { 36 private: 37 /// Start time of the last event in the chunk. 38 ulong lastStartTime; 39 /// Raw profile data. 40 immutable(ubyte)[] data; 41 42 /// Get the start time of the first event in the chunk. 43 ulong startTime() @safe pure nothrow const @nogc 44 { 45 return EventRange(data).front.time; 46 } 47 } 48 49 /** Generates events from the event list as chunks are added. 50 * 51 * Range is not useful here, since it would either have to be 'empty' after consuming 52 * events from existing chunks even though more chunks may be added, or block in 53 * popFront(), which would only make it usable from separate threads/fibers. 54 */ 55 struct Generator 56 { 57 /** A profile event generated by Generator. 58 * 59 * This is a tharsis.prof.Event with some extra data to generate SliceExtents for 60 * zones generated from GeneratedEvents. 61 */ 62 struct GeneratedEvent 63 { 64 private: 65 /// Chunk the event is in. 66 uint chunk; 67 /// The first byte of the event in the chunk. 68 uint startByte; 69 /// The first byte *after* the event in the chunk. 70 uint endByte; 71 72 public: 73 /// Profiling event itself. 74 Event event; 75 76 // Make GeneratedEvent usable as an Event. 77 alias event this; 78 } 79 80 private: 81 /// The chunky event list we are generating events from. 82 const(ChunkyEventList)* events_; 83 /// Index of the current chunk in events_. 84 int chunkIndex_; 85 /// Position of the current event in the current chunk. 86 uint eventPos_ = 0; 87 /// Event range generating events from the current chunk. 88 EventRange currentChunkEvents_; 89 90 public: 91 /** Construct a Generator. 92 * 93 * Params: 94 * 95 * events = Chunky event list to generate events from. 96 */ 97 this(const(ChunkyEventList)* events) @safe pure nothrow @nogc 98 { 99 events_ = events; 100 // If no chunks yet, set 'chunk index' to -1 so we can 'move to the next 101 // chunk' in generate() once there are chunks. 102 if(events_.chunks_.empty) 103 { 104 chunkIndex_ = -1; 105 currentChunkEvents_ = EventRange([]); 106 } 107 // If there already are chunks, use the first one. 108 else 109 { 110 chunkIndex_ = 0; 111 currentChunkEvents_ = EventRange(events_.chunks_[0].data); 112 } 113 } 114 115 /** Try to generate the next event. 116 * 117 * Params: 118 * 119 * event = The event will be written here, if generated. 120 * 121 * Returns: true if an event was generated, false otherwise (all chunks that 122 * have been added to the event list so far have been spent). 123 */ 124 bool generate(out GeneratedEvent event) @safe pure nothrow @nogc 125 { 126 // Done reading current chunk, move to the next one, if any. 127 if(currentChunkEvents_.empty) 128 { 129 // At the end of the last chunk in the list so far. 130 if(chunkIndex_ >= cast(int)events_.chunks_.length - 1) { return false; } 131 132 // There are more chunks, so move to the next. 133 ++chunkIndex_; 134 currentChunkEvents_ = EventRange(events_.chunks_[chunkIndex_].data); 135 eventPos_ = 0; 136 } 137 138 // Generate the event. 139 event.event = currentChunkEvents_.front; 140 event.chunk = chunkIndex_; 141 event.startByte = eventPos_; 142 143 // End pos of the current event, i.e. start pos of the next event. 144 const allBytes = events_.chunks_[chunkIndex_].data.length; 145 eventPos_ = cast(uint)(allBytes - currentChunkEvents_.bytesLeft); 146 147 event.endByte = eventPos_; 148 149 currentChunkEvents_.popFront(); 150 return true; 151 } 152 } 153 154 155 /** A 'slice' of events in the chunky event list. 156 * 157 * Produced by ChunkyEventList.slice() from SliceExtents. SliceExtents are currently 158 * generated only by ChunkyZoneGenerator, which creates exact slices for generated 159 * zones. 160 * 161 * Unlike TimeSlice, which is a slice of all events in specified time, Slice is more 162 * precise; it starts and ends at specific events (TimeSlice includes any events in 163 * specified time, even if multiple events have occured the same time, which can cause 164 * issues with zones when a time slice for a zone includes the zone end event for the 165 * previous zone, which may have ended in the same hectonanosecond as the new zone). 166 */ 167 struct Slice 168 { 169 private: 170 // All chunks in the ChunkyEventList. 171 const(Chunk)[] chunks_; 172 // Extents of this slice (start/end chunk/byte). 173 SliceExtents extents_; 174 175 /* Range over events in the current chunk. 176 * 177 * Once this is empty, the slice is empty (popFront() immediately replaces the 178 * range if emptied while we still have more chunks). 179 */ 180 EventRange currentChunkEvents_; 181 182 // Index of the current chunks in chunks_. 183 uint currentChunk_; 184 185 import std.traits; 186 import std.range; 187 // Must be a forward range of Event. 188 static assert(isForwardRange!Slice, 189 "ChunkyEventList.Slice must be a forward range"); 190 static assert(is(Unqual!(ElementType!Slice) == Event), 191 "ChunkyEventList.Slice must be a range of Event"); 192 193 /* Construct a Slice. 194 * 195 * Params: 196 * 197 * chunks = All chunks in the ChunkyEventList. 198 * slice = Extents of the slice (start/end chunk/byte). 199 */ 200 this(const(Chunk)[] chunks, SliceExtents slice) @safe pure nothrow @nogc 201 { 202 assert(slice.isValid, "Invalid slice in Slice constructor"); 203 204 chunks_ = chunks; 205 extents_ = slice; 206 currentChunk_ = extents_.firstChunk; 207 208 // Must start at chunk start instead of first event pos to ensure any 209 // checkpoint event at the start of the chunk is read - to ensure event times 210 // are correct. 211 currentChunkEvents_ = EventRange(chunks_[currentChunk_].data); 212 const currentChunkLength = chunks_[currentChunk_].data.length; 213 214 for(;;) 215 { 216 const eventEnd = currentChunkLength - currentChunkEvents_.bytesLeft; 217 // If current event is after first event position, we reached the first event. 218 if(eventEnd > extents_.firstEventStart) { break; } 219 currentChunkEvents_.popFront(); 220 } 221 } 222 223 public: 224 /// Get the event on front of the slice. 225 Event front() @safe pure nothrow const @nogc 226 { 227 assert(!empty, "Can't get front of an empty range"); 228 return currentChunkEvents_.front(); 229 } 230 231 /// Move to the next event. 232 void popFront() @safe pure nothrow @nogc 233 { 234 assert(!empty, "Can't pop front of an empty range"); 235 currentChunkEvents_.popFront(); 236 237 // If ran out of events in current chunk, move to the next one. 238 if(currentChunkEvents_.empty) 239 { 240 // Ran out of chunks; the Range is now empty. 241 if(currentChunk_ == extents_.lastChunk) { return; } 242 243 ++currentChunk_; 244 currentChunkEvents_ = EventRange(chunks_[currentChunk_].data); 245 } 246 247 // If we're in the last chunk (or if the last event was at the end of the 248 // last chunk, in which case we've moved into the one after the last). 249 if(currentChunk_ >= extents_.lastChunk) 250 { 251 // If the last popFront()/range replacement 252 const eventEnd = 253 chunks_[currentChunk_].data.length - currentChunkEvents_.bytesLeft; 254 // If we're behind the last chunk, or still in the last chunk but have 255 // reached an event that ends after the last event in extents ends, we're 256 // at the end of the Slice, so make currentChunkEvents_ empty. 257 if(currentChunk_ > extents_.lastChunk || eventEnd > extents_.lastEventEnd) 258 { 259 currentChunkEvents_ = EventRange([]); 260 } 261 } 262 } 263 264 /// Is the slice empty? 265 bool empty() @safe pure nothrow const @nogc { return currentChunkEvents_.empty; } 266 267 268 // Must be a property, isForwardRange won't work otherwise. 269 /// Get a copy of the slice in its current state. 270 @property Slice save() @safe pure nothrow const @nogc { return this; } 271 } 272 273 274 /** A 'slice' of events based on start end end time. 275 * 276 * Produced by ChunkyEventList.timeSlice(). 277 * 278 * TimeSlice is useful to get events in specified time extents but may be useless 279 * for zone generation as it may contain zone end events for zones that started before 280 * the slice. Even if a time slice starting exactly at a zone start time is used, a 281 * preceding zone may have ended in the same hectonanosecond. 282 */ 283 struct TimeSlice 284 { 285 private: 286 // Chunks remaining in the range, not including the chunk used in currentChunkEvents_. 287 const(Chunk)[] chunksLeft_; 288 289 // Range over events in the current chunk. If empty, the TimeSlice is empty. 290 EventRange currentChunkEvents_; 291 292 // Start time of the slice in hectonanosecond. 293 ulong start_; 294 // End time of the slice in hectonanosecond. 295 ulong end_; 296 297 import std.traits; 298 import std.range; 299 // Must be a ForwardRange of Event. 300 static assert(isForwardRange!TimeSlice, 301 "ChunkyEventList.TimeSlice must be a forward range"); 302 static assert(is(Unqual!(ElementType!TimeSlice) == Event), 303 "ChunkyEventList.TimeSlice must be a range of Event"); 304 305 /** Construct a TimeSlice. 306 * 307 * Params: 308 * 309 * chunks = All chunks in the ChunkyEventList. 310 * start = Start time of the slice in hectonanoseconds. 311 * end = End time of the slice in hectonanoseconds. 312 */ 313 this(const(Chunk)[] chunks, ulong start, ulong end) @safe pure nothrow @nogc 314 { 315 // Events starting at start time are included, events ending at end time are not. 316 while(!chunks.empty && chunks.front.lastStartTime < start) { chunks.popFront; } 317 while(!chunks.empty && chunks.back.startTime > end) { chunks.popBack; } 318 319 chunksLeft_ = chunks; 320 start_ = start; 321 end_ = end; 322 323 if(chunksLeft_.empty) 324 { 325 currentChunkEvents_ = EventRange([]); 326 return; 327 } 328 329 // Get an event range for the current chunk and forget the chunk. 330 currentChunkEvents_ = EventRange(chunksLeft_.front.data); 331 chunksLeft_.popFront(); 332 // Move to the first event in the current chunk. 333 while(currentChunkEvents_.front.time < start) 334 { 335 currentChunkEvents_.popFront; 336 } 337 } 338 339 public: 340 /// Get the event on front of the slice. 341 Event front() @safe pure nothrow const @nogc 342 { 343 assert(!empty, "Can't get front of an empty range"); 344 return currentChunkEvents_.front(); 345 } 346 347 /// Move to the next event. 348 void popFront() @safe pure nothrow @nogc 349 { 350 assert(!empty, "Can't pop front of an empty range"); 351 352 currentChunkEvents_.popFront(); 353 354 if(currentChunkEvents_.empty) 355 { 356 // Ran out of chunks; the TimeSlice is now empty. 357 if(chunksLeft_.empty) { return; } 358 359 currentChunkEvents_ = EventRange(chunksLeft_.front.data); 360 chunksLeft_.popFront(); 361 } 362 363 if(currentChunkEvents_.front.time >= end_) 364 { 365 // Ran out of events in our time slice; make currentChunkEvents_ empty. 366 currentChunkEvents_ = EventRange([]); 367 } 368 } 369 370 /// Is the slice empty? 371 bool empty() @safe pure nothrow const @nogc 372 { 373 return currentChunkEvents_.empty; 374 } 375 376 // Must be a property, isForwardRange won't work otherwise. 377 /// Get a copy of the range in its current state. 378 @property TimeSlice save() @safe pure nothrow const @nogc { return this; } 379 } 380 381 /// Extents of a Slice. 382 struct SliceExtents 383 { 384 private: 385 /// Index of the chunk containing the first event in the slice. 386 uint firstChunk; 387 /// Index of the byte in the first chunk where the first event starts. 388 uint firstEventStart; 389 390 /// Index of the chunk containing the last event in the slice. 391 uint lastChunk; 392 /// Index of the byte in the last chunk right after the last event ends. 393 uint lastEventEnd; 394 395 /** Are the SliceExtents valid? 396 * 397 * First chunk must be <= last chunk and the extents must not be empty. 398 */ 399 bool isValid() @safe pure nothrow const @nogc 400 { 401 return firstChunk <= lastChunk && 402 (firstChunk != lastChunk || firstEventStart < lastEventEnd); 403 } 404 } 405 406 private: 407 /** Storage for chunks (chunk slices, not chunk data itself). 408 * 409 * Passed by constructor or provideStorage(). Never reallocated internally. 410 */ 411 Chunk[] chunkStorage_; 412 413 /// A slice of chunkStorage_ that contains actually used chunks. 414 Chunk[] chunks_; 415 416 public: 417 /** Construct a ChunkyEventList. 418 * 419 * Params: 420 * 421 * chunkStorage = Space allocated for profile data chunks (not chunk data itself). 422 * outOfSpace() must be called before adding chunks to determine if 423 * this space has been spent, and provideStorage() must be called 424 * to allocate more chunks after running out of space. ChunkyEventList 425 * never allocates by itself. 426 */ 427 this(Chunk[] chunkStorage) @safe pure nothrow @nogc 428 { 429 chunkStorage_ = chunkStorage; 430 } 431 432 /** Is the ChunkyEventList out of space? 433 * 434 * If true, more chunk storage must be provided by calling provideStorage(). 435 */ 436 bool outOfSpace() @safe pure nothrow const @nogc 437 { 438 return chunks_.length >= chunkStorage_.length; 439 } 440 441 /** Provide more space to store chunks (not chunk data itself). 442 * 443 * Must be called when outOfSpace() returns true. Must provide more space than the 444 * preceding provideStorage() or constructor call. 445 */ 446 void provideStorage(Chunk[] storage) @safe pure nothrow @nogc 447 { 448 assert(storage.length >= chunks_.length, 449 "provideStorage does not provide enough space for existing chunks"); 450 451 chunkStorage_ = storage; 452 chunkStorage_[0 .. chunks_.length] = chunks_[]; 453 chunks_ = chunkStorage_[0 .. chunks_.length]; 454 } 455 456 /// Get a generator to produce profiling events from the list over time as chunks are added. 457 Generator generator() @safe pure nothrow const @nogc 458 { 459 return Generator(&this); 460 } 461 462 /** Add a new chunk of profile data. 463 * 464 * Params: 465 * 466 * data = Chunk of data to add. Note that the first event in the chunk must have 467 * higher time value for the chunk to be added (false will be returned on 468 * error). This can be ensured by emitting a checkpoint event with the Profiler 469 * that produces the chunk before any other events in the chunk. Also note that 470 * data $(B must not) be deallocated for as long as the ChunkyEventList exists; 471 * the ChunkyEventList will use data directly instead of creating a copy. 472 * 473 * Returns: true on success, false if the first event in the chunk didn't occur in 474 * time after the last event already in the list. 475 */ 476 bool addChunk(immutable(ubyte)[] data) @safe pure nothrow @nogc 477 { 478 assert(!data.empty, "Can't add an empty chunk of profiling data"); 479 assert(chunks_.length < chunkStorage_.length, "Out of chunk space"); 480 481 // Get the start time of the last event in the chunk. 482 ulong lastStartTime; 483 foreach(event; EventRange(data)) { lastStartTime = event.time; } 484 485 auto newChunk = Chunk(lastStartTime, data); 486 487 // New chunk must start at or after the end of the last chunk. 488 if(!chunks_.empty && newChunk.startTime < chunks_.back.lastStartTime) { return false; } 489 chunkStorage_[chunks_.length] = newChunk; 490 chunks_ = chunkStorage_[0 .. chunks_.length + 1]; 491 return true; 492 } 493 494 /** Get an exact slice of the ChunkyEventList as described by a SliceExtents instance. 495 * 496 * SliceExtents is currently only generated by the ChunkyZoneGenerator to allow 497 * getting exact slices containing only the events in any single zone, as opposed to 498 * all events that occured at the time of that zone (e.g. an end of a preceding zone 499 * that occured in the same hectonanosecond a new zone started in). 500 */ 501 Slice slice(SliceExtents slice) @safe pure nothrow const @nogc 502 { 503 return Slice(chunks_, slice); 504 } 505 506 /** Get a slice of the ChunkyEventList containing events in specified time range. 507 * 508 * Params: 509 * 510 * start = Start of the time slice. Events occuring at this time will be included. 511 * end = End of the time slice. Events occuring at this time will $(D not) be included. 512 */ 513 TimeSlice timeSlice(ulong start, ulong end) @safe pure nothrow const @nogc 514 { 515 return TimeSlice(chunks_, start, end); 516 } 517 } 518 unittest 519 { 520 writeln("ChunkyEventList unittest"); 521 scope(success) { writeln("ChunkyEventList unittest SUCCESS"); } 522 scope(failure) { writeln("ChunkyEventList unittest FAILURE"); } 523 524 const frameCount = 16; 525 526 import std.typecons; 527 auto profiler = scoped!Profiler(new ubyte[Profiler.maxEventBytes + 2048]); 528 auto chunkyEvents = ChunkyEventList(new ChunkyEventList.Chunk[frameCount + 1]); 529 530 size_t lastChunkEnd = 0; 531 profiler.checkpointEvent(); 532 // std.typecons.scoped! stores the Profiler on the stack. 533 // Simulate 16 'frames' 534 foreach(frame; 0 .. frameCount) 535 { 536 Zone topLevel = Zone(profiler, "frame"); 537 538 // Simulate frame overhead. Replace this with your frame code. 539 { 540 Zone nested1 = Zone(profiler, "frameStart"); 541 foreach(i; 0 .. 1000) { continue; } 542 } 543 { 544 Zone nested2 = Zone(profiler, "frameCore"); 545 foreach(i; 0 .. 10000) { continue; } 546 } 547 548 if(!chunkyEvents.addChunk(profiler.profileData[lastChunkEnd .. $].idup)) 549 { 550 assert(false); 551 } 552 lastChunkEnd = profiler.profileData.length; 553 profiler.checkpointEvent(); 554 } 555 556 // Chunk for the last checkpoint/zone end. 557 if(!chunkyEvents.addChunk(profiler.profileData[lastChunkEnd .. $].idup)) 558 { 559 assert(false); 560 } 561 562 // Ensure a slice of all events in chunkyEvents contains all events. 563 assert(EventRange(profiler.profileData).equal(chunkyEvents.timeSlice(0, ulong.max))); 564 565 // Ensure events in a time slice actually are in that time slice. 566 foreach(event; chunkyEvents.timeSlice(1000, 3000)) 567 { 568 assert(event.time >= 1000 && event.time < 3000); 569 } 570 } 571 572 /// Readability alias. 573 alias ChunkyEventGenerator = ChunkyEventList.Generator; 574 /// Readability alias. 575 alias ChunkyEventSlice = ChunkyEventList.Slice; 576 577 578 /** Generates zones from a ChunkyEventList as chunks are added. 579 * 580 * Range is not useful here, since it would either have to be 'empty' after consuming 581 * zones from existing chunks even though more chunks may be added, or block in 582 * popFront(), which would only make it usable from separate threads/fibers. 583 */ 584 struct ChunkyZoneGenerator 585 { 586 /// ZoneData extended with ChunkyEventList slice extents to regenerate events in the zone. 587 struct GeneratedZoneData 588 { 589 /** ChunkyEventList extents of all events used to produce this zone. 590 * 591 * Allows to slice the ChunkyEventList to reproduce the zone and its children. 592 */ 593 ChunkyEventList.SliceExtents extents; 594 595 /// The zone data itself. 596 ZoneData zoneData; 597 alias zoneData this; 598 } 599 600 private: 601 /** ZoneInfo extended with information about the chunk and byte containing the first event. 602 * 603 * Needed to initialize the SliceExtents in GeneratedZoneData. 604 */ 605 struct ExtendedZoneInfo 606 { 607 // Chunk containing the first event in the zone (its zone start event). 608 uint firstChunk; 609 // First byte of the first event in firstChunk. 610 uint startByte; 611 612 // Generated zone info itself. 613 ZoneInfo zoneInfo; 614 615 // Use ExtendedZoneInfo as ZoneInfo. 616 alias zoneInfo this; 617 } 618 619 // Generates profiling events as chunks are added. 620 ChunkyEventGenerator events_; 621 622 // Stack of ZoneInfo describing the current zone and all its parents. 623 // 624 // The current zone can be found at zoneStack_[zoneStackDepth_ - 1], its parent 625 // at zoneStack_[zoneStackDepth_ - 2], etc. 626 ExtendedZoneInfo[maxStackDepth] zoneStack_; 627 628 // Depth of the zone stack at the moment. 629 size_t zoneStackDepth_ = 0; 630 631 // ID of the next zone. 632 uint nextID_ = 1; 633 634 public: 635 /** Construct a ChunkyZoneRange. 636 * 637 * Params: 638 * 639 * eventList = Chunky event generator (returned by ChunkyEventList.generator()) to 640 * produce events to generate zones from. 641 */ 642 this(ChunkyEventGenerator events) @safe pure nothrow @nogc 643 { 644 events_ = events; 645 } 646 647 /** Try to generate the next zone. 648 * 649 * Params: 650 * 651 * zone = The zone will be written here, if generated. 652 * 653 * Returns: true if an zone was generated, false otherwise (all chunks that have been 654 * added to the event list so far have been spent). 655 */ 656 bool generate(out GeneratedZoneData zone) @safe pure nothrow @nogc 657 { 658 events_.GeneratedEvent event; 659 while(events_.generate(event)) 660 { 661 alias stack = zoneStack_; 662 alias depth = zoneStackDepth_; 663 with(EventID) final switch(event.id) 664 { 665 case Checkpoint, Variable: break; 666 case ZoneStart: 667 assert(zoneStackDepth_ < maxStackDepth, 668 "Zone nesting too deep; zone stack overflow."); 669 const zoneInfo = ZoneInfo(nextID_++, event.time); 670 stack[depth++] = ExtendedZoneInfo(event.chunk, event.startByte, zoneInfo); 671 break; 672 case ZoneEnd: 673 zone.zoneData = buildZoneData(stack[0 .. depth], event.time); 674 ExtendedZoneInfo info = stack[depth - 1]; 675 alias Extents = ChunkyEventList.SliceExtents; 676 zone.extents = Extents(info.firstChunk, info.startByte, 677 event.chunk, event.endByte); 678 --depth; 679 return true; 680 // If an info event has the same start time as the current zone, it's info 681 // about the current zone. 682 case Info: 683 auto curZone = &stack[depth - 1]; 684 if(event.time == curZone.startTime) { curZone.info = event.info; } 685 break; 686 } 687 } 688 689 return false; 690 } 691 } 692 unittest 693 { 694 writeln("ChunkyZoneGenerator unittest"); 695 scope(success) { writeln("ChunkyZoneGenerator unittest SUCCESS"); } 696 scope(failure) { writeln("ChunkyZoneGenerator unittest FAILURE"); } 697 698 699 const frameCount = 16; 700 701 import std.typecons; 702 auto profiler = scoped!Profiler(new ubyte[Profiler.maxEventBytes + 2048]); 703 auto chunkyEvents = ChunkyEventList(new ChunkyEventList.Chunk[frameCount + 1]); 704 auto chunkyZones = ChunkyZoneGenerator(chunkyEvents.generator); 705 706 size_t lastChunkEnd = 0; 707 profiler.checkpointEvent(); 708 // std.typecons.scoped! stores the Profiler on the stack. 709 // Simulate 16 'frames' 710 foreach(frame; 0 .. frameCount) 711 { 712 Zone topLevel = Zone(profiler, "frame"); 713 714 // Simulate frame overhead. Replace this with your frame code. 715 { 716 Zone nested1 = Zone(profiler, "frameStart"); 717 foreach(i; 0 .. 1000) { continue; } 718 } 719 { 720 Zone nested2 = Zone(profiler, "frameCore"); 721 foreach(i; 0 .. 10000) { continue; } 722 } 723 724 if(!chunkyEvents.addChunk(profiler.profileData[lastChunkEnd .. $].idup)) 725 { 726 assert(false); 727 } 728 729 chunkyZones.GeneratedZoneData zone; 730 // "frame" zone from the previous frame (in current frame it's still in progress) 731 if(frame > 0 && !chunkyZones.generate(zone)) { assert(false); } 732 assert(frame == 0 || zone.info == "frame"); 733 // "frameStart" from current frame 734 if(!chunkyZones.generate(zone)) { assert(false); } 735 736 assert(zone.info == "frameStart" && 737 zone.id == frame * 3 + 2 && 738 zone.parentID == frame * 3 + 1 739 && zone.nestLevel == 2); 740 // "frameCore" from current frame 741 if(!chunkyZones.generate(zone)) { assert(false); } 742 assert(zone.info == "frameCore" && 743 zone.id == frame * 3 + 3 && 744 zone.parentID == frame * 3 + 1 745 && zone.nestLevel == 2); 746 747 lastChunkEnd = profiler.profileData.length; 748 profiler.checkpointEvent(); 749 } 750 751 // Chunk for the last checkpoint/zone end. 752 if(!chunkyEvents.addChunk(profiler.profileData[lastChunkEnd .. $].idup)) 753 { 754 assert(false); 755 } 756 }