@@ -31,21 +31,72 @@ class Worker {
3131 }
3232}
3333
34+ interface TimePeriod {
35+ label : string
36+ // duration in seconds
37+ duration : number | null
38+ }
39+
40+ const TIME_PERIODS : TimePeriod [ ] = [
41+ { label : "Latest" , duration : null } ,
42+ { label : "1 hour" , duration : 1 * 60 * 60 } ,
43+ { label : "1 day" , duration : 24 * 60 * 60 } ,
44+ { label : "7 days" , duration : 7 * 24 * 60 * 60 } ,
45+ { label : "1 month" , duration : 30 * 24 * 60 * 60 } ,
46+ { label : "3 months" , duration : 90 * 24 * 60 * 60 } ,
47+ ]
48+
3449function formatTimestamp ( timestamp : number ) : string {
3550 const date = new Date ( timestamp * 1000 )
3651 return date . toLocaleString ( )
3752}
3853
39- // 24 hours
40- const DEFAULT_RANGE_DURATION = 24 * 60 * 60
54+ // tolerance for a region, in seconds
55+ const REGION_TOLERANCE = 5 * 60
56+
57+ function segmentRegions ( segments : Segment [ ] ) : [ number , number ] [ ] {
58+ // returns a list of [start, end] regions, where a region is defined as the largest
59+ // interval where there is no timestamp without an active segment.
60+ // so in
61+ //
62+ // ```
63+ // [--] [-------]
64+ // [---] [-] [------]
65+ // [----] [--]
66+ // ```
67+ // there are 3 regions.
68+
69+ // We iterate over the egments in order of start time. We track the latest seen end time.
70+ // If we ever see a segment with a later start time than the current end time, that means
71+ // there must have been empty space between them, which marks a new region.
72+
73+ // assert segments are sorted by segment.start
74+ console . assert (
75+ segments . every (
76+ ( segment , index ) => index === 0 || segment . start >= segments [ index - 1 ] . start ,
77+ ) ,
78+ )
79+
80+ if ( segments . length == 0 ) {
81+ return [ ]
82+ }
4183
42- function niceDefaultRange (
43- minTimestamp : number ,
44- maxTimestamp : number ,
45- ) : [ number , number ] {
46- // by default: show from maxTimestamp at the end, to DEFAULT_RANGE_DURATION seconds before
47- // that at the start.
48- return [ Math . max ( minTimestamp , maxTimestamp - DEFAULT_RANGE_DURATION ) , maxTimestamp ]
84+ let regions : [ number , number ] [ ] = [ ]
85+ let regionStart = segments [ 0 ] . start
86+ let latestEnd = segments [ 0 ] . end
87+ for ( const segment of segments ) {
88+ if ( segment . start > latestEnd + REGION_TOLERANCE ) {
89+ // this marks a new region
90+ regions . push ( [ regionStart , latestEnd ] )
91+ regionStart = segment . start
92+ }
93+
94+ latestEnd = Math . max ( latestEnd , segment . end )
95+ }
96+
97+ // finalize the current region
98+ regions . push ( [ regionStart , latestEnd ] )
99+ return regions
49100}
50101
51102function nodeColor ( nodeid : string ) : string {
@@ -163,6 +214,8 @@ export function WorkersPage() {
163214 const navigate = useNavigate ( )
164215 const { showTooltip, hideTooltip, moveTooltip } = useTooltip ( )
165216 const [ expandedWorkers , setExpandedWorkers ] = useState < Set < string > > ( new Set ( ) )
217+ const [ selectedPeriod , setSelectedPeriod ] = useState < TimePeriod > ( TIME_PERIODS [ 0 ] ) // Default to "Latest"
218+ const [ userRange , setUserRange ] = useState < [ number , number ] | null > ( null )
166219
167220 const workerUuids = OrderedSet (
168221 Array . from ( tests . values ( ) )
@@ -230,14 +283,65 @@ export function WorkersPage() {
230283 } )
231284
232285 workers . sortKey ( worker => worker . segments [ 0 ] . start )
233-
234- const [ visibleRange , setVisibleRange ] = useState < [ number , number ] > (
235- niceDefaultRange ( minTimestamp , maxTimestamp ) ,
286+ const segments = workers
287+ . flatMap ( worker => worker . segments )
288+ . sortKey ( segment => segment . start )
289+ const regions = segmentRegions ( segments )
290+
291+ const span = maxTimestamp - minTimestamp
292+ // find the first time period which is larger than the span of the workers.
293+ // that time period is available, but anything after is not.
294+ const firstLargerPeriod = TIME_PERIODS . findIndex (
295+ period => period . duration !== null && period . duration >= span ,
236296 )
237297
298+ function getSliderRange ( ) : [ number , number ] {
299+ if ( selectedPeriod . duration === null ) {
300+ const latestRegion = regions [ regions . length - 1 ]
301+ // the range is just the last region, unless there are no segments, in which case
302+ // we use the min/max timestamp
303+ return regions . length > 0
304+ ? [ latestRegion [ 0 ] , latestRegion [ 1 ] ]
305+ : [ minTimestamp , maxTimestamp ]
306+ }
307+
308+ const range : [ number , number ] = [
309+ Math . max ( minTimestamp , maxTimestamp - selectedPeriod . duration ! ) ,
310+ maxTimestamp ,
311+ ]
312+
313+ // trim the slider range to remove any time at the beginning or end when there
314+ // are no active workers
315+ let trimmedMin : number | null = null
316+ let trimmedMax : number | null = null
317+ for ( const worker of workers ) {
318+ const visibleSegments = worker . visibleSegments ( range )
319+ if ( visibleSegments . length === 0 ) {
320+ continue
321+ }
322+
323+ if ( trimmedMin === null || visibleSegments [ 0 ] . start < trimmedMin ) {
324+ trimmedMin = visibleSegments [ 0 ] . start
325+ }
326+
327+ if (
328+ trimmedMax === null ||
329+ visibleSegments [ visibleSegments . length - 1 ] . end > trimmedMax
330+ ) {
331+ trimmedMax = visibleSegments [ visibleSegments . length - 1 ] . end
332+ }
333+ }
334+
335+ return [ trimmedMin ?? range [ 0 ] , trimmedMax ?? range [ 1 ] ]
336+ }
337+ const sliderRange = getSliderRange ( )
338+ const visibleRange = userRange ?? sliderRange
339+
238340 useEffect ( ( ) => {
239- setVisibleRange ( niceDefaultRange ( minTimestamp , maxTimestamp ) )
240- } , [ minTimestamp , maxTimestamp ] )
341+ // reset the range when clicking on a period, even if it's the same period. This gives a
342+ // nice "reset button" ux to users.
343+ setUserRange ( null )
344+ } , [ selectedPeriod ] )
241345
242346 const [ visibleMin , visibleMax ] = visibleRange
243347 const visibleDuration = visibleMax - visibleMin
@@ -287,12 +391,30 @@ export function WorkersPage() {
287391 </ div >
288392 < div className = "workers" >
289393 < div className = "workers__controls" >
394+ < div className = "workers__durations" >
395+ { TIME_PERIODS . map ( ( period , index ) => {
396+ const available = index <= firstLargerPeriod
397+ return (
398+ < div
399+ key = { index }
400+ className = { `workers__durations__button ${
401+ selectedPeriod . label === period . label
402+ ? "workers__durations__button--active"
403+ : ""
404+ } ${ ! available ? "workers__durations__button--disabled" : "" } `}
405+ onClick = { ( ) => available && setSelectedPeriod ( period ) }
406+ >
407+ { period . label }
408+ </ div >
409+ )
410+ } ) }
411+ </ div >
290412 < RangeSlider
291- min = { minTimestamp }
292- max = { maxTimestamp }
413+ min = { sliderRange [ 0 ] }
414+ max = { sliderRange [ 1 ] }
293415 value = { visibleRange }
294- onChange = { newRange => setVisibleRange ( newRange ) }
295- step = { ( maxTimestamp - minTimestamp ) / 1000 }
416+ onChange = { newRange => setUserRange ( newRange ) }
417+ step = { ( sliderRange [ 1 ] - sliderRange [ 0 ] ) / 1000 }
296418 />
297419 </ div >
298420 < div className = "workers__timeline-header" >
0 commit comments