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 }