querysub 0.458.0 → 0.459.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "querysub",
3
- "version": "0.458.0",
3
+ "version": "0.459.0",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
@@ -69,7 +69,7 @@ const renderChartPNG = cacheArgsEqual((
69
69
  height: number,
70
70
  nodeIds: readonly string[],
71
71
  bucketsArrays: readonly GrossStatsBucket[][],
72
- ): string => {
72
+ ): { pngUrl: string; maxTotal: number } => {
73
73
  let canvas = document.createElement("canvas");
74
74
  canvas.width = width;
75
75
  canvas.height = height;
@@ -115,7 +115,7 @@ const renderChartPNG = cacheArgsEqual((
115
115
  }
116
116
  }
117
117
 
118
- return canvas.toDataURL();
118
+ return { pngUrl: canvas.toDataURL(), maxTotal };
119
119
  }, 5);
120
120
 
121
121
  export class GrossStatsPage extends qreact.Component {
@@ -136,23 +136,31 @@ export class GrossStatsPage extends qreact.Component {
136
136
  <div className={css.hbox(6)}>
137
137
  <span>Range:</span>
138
138
  {TIME_RANGES.map(r =>
139
- <span
140
- className={css.button.pad2(4, 2) + (state.rangeMs === r.ms ? " " + css.hsl(210, 70, 60).hslcolor(0, 0, 100) : "")}
139
+ <button
140
+ className={css.pad2(8, 4) + (state.rangeMs === r.ms ? " " + css.hsl(210, 70, 60).hslcolor(0, 0, 100).bold : "")}
141
141
  onClick={() => Querysub.commit(() => { state.rangeMs = r.ms; })}
142
- >{r.label}</span>
142
+ >{r.label}</button>
143
143
  )}
144
144
  </div>
145
145
  <div className={css.hbox(6).wrap}>
146
146
  <span>Field:</span>
147
147
  {GROSS_STATS_FIELDS.map(f =>
148
- <span
149
- className={css.button.pad2(4, 2) + (state.selectedField === f ? " " + css.hsl(140, 60, 50).hslcolor(0, 0, 100) : "")}
148
+ <button
149
+ className={css.pad2(8, 4) + (state.selectedField === f ? " " + css.hsl(140, 60, 50).hslcolor(0, 0, 100).bold : "")}
150
150
  onClick={() => Querysub.commit(() => { state.selectedField = f; })}
151
- >{f}</span>
151
+ >{f}</button>
152
152
  )}
153
153
  </div>
154
154
  <div className={css.hbox(8).wrap}>
155
155
  <span>Servers:</span>
156
+ <button
157
+ className={css.pad2(8, 4)}
158
+ onClick={() => Querysub.commit(() => { state.excludedNodes = new Set(); })}
159
+ >Select all</button>
160
+ <button
161
+ className={css.pad2(8, 4)}
162
+ onClick={() => Querysub.commit(() => { state.excludedNodes = new Set(nodeIds); })}
163
+ >Select none</button>
156
164
  {nodeIds.map((nodeId, i) =>
157
165
  <label className={css.hbox(4)}>
158
166
  <input
@@ -198,6 +206,15 @@ export class GrossStatsPage extends qreact.Component {
198
206
  perNodeTotals.push(totals);
199
207
  }
200
208
 
209
+ let maxPerField = {} as Record<GrossStatsField, number>;
210
+ for (let f of GROSS_STATS_FIELDS) {
211
+ maxPerField[f] = 0;
212
+ for (let totals of perNodeTotals) {
213
+ if (totals[f] > maxPerField[f]) maxPerField[f] = totals[f];
214
+ }
215
+ }
216
+ let highlightStyle = css.hsl(60, 90, 75);
217
+
201
218
  return <table className={css.fillWidth}>
202
219
  <thead>
203
220
  <tr>
@@ -215,9 +232,12 @@ export class GrossStatsPage extends qreact.Component {
215
232
  <td className={css.pad2(4)} style={{ borderLeft: `4px solid ${colorForNode(i)}` }}>
216
233
  {shortNodeId(nodeId)}
217
234
  </td>
218
- {GROSS_STATS_FIELDS.map(f =>
219
- <td className={css.textAlign("right").pad2(4)}>{formatNumber(perNodeTotals[i][f])}</td>
220
- )}
235
+ {GROSS_STATS_FIELDS.map(f => {
236
+ let isMax = perNodeTotals[i][f] > 0 && perNodeTotals[i][f] === maxPerField[f];
237
+ return <td className={css.textAlign("right").pad2(4) + (isMax ? " " + highlightStyle : "")}>
238
+ {formatNumber(perNodeTotals[i][f])}
239
+ </td>;
240
+ })}
221
241
  </tr>
222
242
  )}
223
243
  </tbody>
@@ -234,7 +254,7 @@ export class GrossStatsPage extends qreact.Component {
234
254
  let bucketsArrays: GrossStatsBucket[][] = selectedNodeIds.map(n => bucketsByNode.get(n) ?? []);
235
255
  let anyLoading = !result;
236
256
 
237
- let chartPNG = renderChartPNG(
257
+ let chart = renderChartPNG(
238
258
  state.selectedField,
239
259
  state.rangeMs,
240
260
  CHART_WIDTH,
@@ -242,18 +262,36 @@ export class GrossStatsPage extends qreact.Component {
242
262
  selectedNodeIds,
243
263
  bucketsArrays,
244
264
  );
265
+ let now = Date.now();
266
+ let windowStart = now - state.rangeMs;
245
267
 
246
268
  return <div className={css.vbox(8).pad2(8).fillWidth}>
247
269
  <h2>Cluster Stats</h2>
248
270
  {this.renderControls(allNodeIds)}
249
- <img
250
- src={chartPNG}
251
- width={CHART_WIDTH}
252
- height={CHART_HEIGHT}
253
- className={css.hsl(0, 0, 100)}
254
- style={anyLoading ? { opacity: 0.5 } : undefined}
255
- />
271
+ <div className={css.vbox(4)}>
272
+ <div className={css.hbox(8).bold}>
273
+ <span>peak: {formatNumber(chart.maxTotal)} {state.selectedField} / minute</span>
274
+ </div>
275
+ <img
276
+ src={chart.pngUrl}
277
+ width={CHART_WIDTH}
278
+ height={CHART_HEIGHT}
279
+ className={css.hsl(0, 0, 100)}
280
+ style={anyLoading ? { opacity: 0.5 } : undefined}
281
+ />
282
+ <div className={css.hbox(0).fillWidth}>
283
+ <span className={css.flexShrink0}>{formatLocalTime(windowStart)}</span>
284
+ <span className={css.fillBoth}></span>
285
+ <span className={css.flexShrink0}>{formatLocalTime(now)}</span>
286
+ </div>
287
+ </div>
256
288
  {this.renderTable(selectedNodeIds, bucketsArrays)}
257
289
  </div>;
258
290
  }
259
291
  }
292
+
293
+ function formatLocalTime(ms: number): string {
294
+ let d = new Date(ms);
295
+ let pad = (n: number) => String(n).padStart(2, "0");
296
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
297
+ }
@@ -393,7 +393,16 @@ export class BufferIndex {
393
393
  const bufferStep = iterateForward ? 1 : -1;
394
394
 
395
395
  for (let bufferIndex = bufferStartIdx; iterateForward ? bufferIndex < bufferEndIdx : bufferIndex > bufferEndIdx; bufferIndex += bufferStep) {
396
- if (matchCount >= params.limit || !config.keepIterating()) break;
396
+ // No `matchCount >= params.limit` cap inside the block.
397
+ // Buffer order within a block is not guaranteed to follow
398
+ // the search direction (blocks are time-ordered, buffers
399
+ // inside them are not), so stopping mid-block on a match
400
+ // count would drop earlier-time buffers we haven't reached
401
+ // yet. The block-level cap above is the only safe stop;
402
+ // here we only honor cross-file `keepIterating` (which
403
+ // applies to the whole file at once, so it's safe at any
404
+ // granularity).
405
+ if (!config.keepIterating()) break;
397
406
  await config.results.limitGroup?.wait();
398
407
 
399
408
  let buffer = buffers[bufferIndex];
@@ -580,7 +580,16 @@ export class BufferUnitIndex {
580
580
  const step = iterateForward ? 1 : -1;
581
581
 
582
582
  for (let i = startIdx; iterateForward ? i < endIdx : i > endIdx; i += step) {
583
- if (stopIterating()) break;
583
+ // No matchCount-based cap inside the block. Buffer
584
+ // order within a block is not guaranteed to follow the
585
+ // search direction (blocks are time-ordered, buffers
586
+ // inside them are not), so a mid-block stop on
587
+ // `relevantCount >= limit` would drop earlier-time
588
+ // buffers we haven't reached yet. Block-level
589
+ // `stopIterating` is the safe granularity; here we
590
+ // only honor cross-file `keepIterating`, which applies
591
+ // to the whole file at once.
592
+ if (!keepIterating()) break;
584
593
  await results.limitGroup?.wait();
585
594
 
586
595
  const buffer = await this.getBufferFromBlock(blockReader, i);