14
14
* limitations under the License.
15
15
*/
16
16
17
+ import Heap from "heap"
17
18
import stripAnsi from "strip-ansi"
18
19
import type { TextProps } from "ink"
19
20
20
21
import type { Tail } from "../tailf.js"
21
22
import type { OnData , Worker } from "../../../components/Dashboard/types.js"
22
23
23
- import { WorkerState , stateFor } from "./states.js"
24
+ import { WorkerState , rankFor , stateFor } from "./states.js"
25
+
26
+ type Line = { line : string ; stateRank : number ; timestamp : number }
24
27
25
28
/**
26
29
* Maintain a model of live data from a given set of file streams
@@ -32,16 +35,21 @@ export default class Live {
32
35
private readonly workers : Record < string , Worker > = { }
33
36
34
37
/** Number of lines of output to retain. TODO this depends on height of terminal? */
35
- private static readonly nLines = 18
36
-
37
- /** Model of the last `Live.nLines` lines of output */
38
- private readonly lines : string [ ] = Array ( Live . nLines ) . fill ( "" )
38
+ private static readonly MAX_HEAP = 1000
39
39
40
- /** Current insertion index into `this.lines` */
41
- private linesInsertionIdx = 0
40
+ /** Model of the lines of output */
41
+ private readonly lines = new Heap < Line > ( ( a , b ) => {
42
+ if ( a . line === b . line ) {
43
+ return 0
44
+ }
42
45
43
- /** Current number of valid lines in `this.lines` */
44
- private linesCount = 0
46
+ const stateDiff = a . stateRank - b . stateRank
47
+ if ( stateDiff !== 0 ) {
48
+ return stateDiff
49
+ } else {
50
+ return a . timestamp - b . timestamp
51
+ }
52
+ } )
45
53
46
54
public constructor ( private readonly tails : Promise < Tail > [ ] , cb : OnData , styleOf : Record < WorkerState , TextProps > ) {
47
55
tails . map ( ( tailf ) => {
@@ -60,7 +68,7 @@ export default class Live {
60
68
61
69
if ( ! name || ! timestamp ) {
62
70
// console.error("Bad status record", line)
63
- this . pushLineAndPublish ( data , cb )
71
+ // this.pushLineAndPublish(data, metric, timestamp , cb)
64
72
return
65
73
} else if ( ! metric ) {
66
74
// ignoring this line
@@ -93,7 +101,7 @@ export default class Live {
93
101
94
102
// inform the UI that we have updates
95
103
cb ( {
96
- lines : this . pushLine ( data ) ,
104
+ lines : this . pushLine ( data , metric , timestamp ) ,
97
105
workers : Object . values ( this . workers ) ,
98
106
} )
99
107
}
@@ -112,40 +120,45 @@ export default class Live {
112
120
} )
113
121
}
114
122
123
+ private readonly lookup : Record < string , Line > = { }
115
124
/** Add `line` to our circular buffer `this.lines` */
116
- private pushLine ( line : string ) {
117
- if ( this . lines . includes ( line ) ) {
118
- // duplicate line
119
- // the oldest is the one we are about to overwrite
120
- let oldestIdx = this . linesInsertionIdx
121
- if ( this . lines [ oldestIdx ] . length === 0 ) {
122
- do {
123
- oldestIdx = ( oldestIdx + 1 ) % Live . nLines
124
- } while ( this . lines [ oldestIdx ] . length === 0 )
125
- }
126
- return { lines : this . lines , idx : oldestIdx , N : this . linesCount }
125
+ private pushLine ( line : string , metric : WorkerState , timestamp : number ) {
126
+ const key = line
127
+ . replace ( / \s * ( \d \d \d \d - \d \d - \d \d T \d \d : \d \d : \d \d Z ) \s * / , "{timestamp}" )
128
+ . replace ( / p o d \/ t o r c h x - \S + / , "" ) // worker name in torchx
129
+ . replace ( / p o d \/ r a y - ( h e a d | w o r k e r ) - \S + / , "" ) // worker name in ray
130
+ . replace ( / \* / , "" ) // wildcard worker name (codeflare)
131
+ . replace ( / \x1b \x5B \[ 2 J / g, "" ) // eslint-disable-line no-control-regex
132
+ // ^^^ [2J is part of clear screen; we don't want those to flow through
133
+
134
+ const rec = {
135
+ timestamp,
136
+ stateRank : rankFor [ metric ] ,
137
+ line : key ,
138
+ }
139
+
140
+ const already = this . lookup [ rec . line ]
141
+ if ( already ) {
142
+ already . timestamp = timestamp
143
+ this . lines . updateItem ( already )
127
144
} else {
128
- const idx = this . linesInsertionIdx
129
- this . linesInsertionIdx = ( this . linesInsertionIdx + 1 ) % Live . nLines
130
-
131
- this . lines [ idx ] = line
132
- this . linesCount = Math . min ( this . lines . length , this . linesCount + 1 )
133
-
134
- // the oldest is the one we are about to overwrite
135
- let oldestIdx = this . linesInsertionIdx
136
- if ( this . lines [ oldestIdx ] . length === 0 ) {
137
- do {
138
- oldestIdx = ( oldestIdx + 1 ) % Live . nLines
139
- } while ( this . lines [ oldestIdx ] . length === 0 )
145
+ this . lookup [ rec . line ] = rec
146
+ if ( this . lines . size ( ) >= Live . MAX_HEAP ) {
147
+ this . lines . replace ( rec )
148
+ } else {
149
+ this . lines . push ( rec )
140
150
}
141
-
142
- return { lines : this . lines , idx : oldestIdx , N : this . linesCount }
143
151
}
152
+
153
+ return this . lines
154
+ . toArray ( )
155
+ . slice ( 0 , 18 )
156
+ . sort ( ( a , b ) => a . timestamp - b . timestamp )
144
157
}
145
158
146
159
/** `pushLine` and then pass the updated model to `cb` */
147
- private pushLineAndPublish ( line : string , cb : OnData ) {
148
- cb ( { lines : this . pushLine ( line ) , workers : Object . values ( this . workers ) } )
160
+ private pushLineAndPublish ( line : string , metric : WorkerState , timestamp : number , cb : OnData ) {
161
+ cb ( { lines : this . pushLine ( line , metric , timestamp ) , workers : Object . values ( this . workers ) } )
149
162
}
150
163
151
164
private asMillisSinceEpoch ( timestamp : string ) {
0 commit comments