@agoric/telemetry 0.6.3-other-dev-8f8782b.0 → 0.6.3-other-dev-fbe72e7.0.fbe72e7

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.
@@ -1,23 +1,15 @@
1
+ // @ts-check
2
+ /* eslint-env node */
1
3
  /// <reference types="ses" />
2
4
 
3
- // https://github.com/Agoric/agoric-sdk/issues/3742#issuecomment-1028451575
4
- // I'd mmap() a 100MB file, reserve a few bytes for offsets, then use the rest
5
- // as a circular buffer to hold length-prefixed records. The agd process would
6
- // keep writing new events into the RAM window and updating the start/end
7
- // pointers, with some sequencing to make sure the record gets written before
8
- // the pointer is updated. Then, no mattter how abruptly the process is
9
- // terminated, as long as the host computer itself is still running, the on-disk
10
- // file would contain the most recent state, and anybody who reads the file will
11
- // get the most recent state. The host kernel (linux) is under no obligation to
12
- // flush it to disk any particular time, but knows when reads happen, so there's
13
- // no coherency problem, and the speed is unaffected by disk write speeds.
14
-
15
- import BufferFromFile from 'bufferfromfile';
16
- import { promises as fsPromises } from 'fs';
17
- import path from 'path';
5
+ import fsp from 'node:fs/promises';
6
+ import path from 'node:path';
7
+ import { Fail } from '@endo/errors';
18
8
  import { serializeSlogObj } from './serialize-slog-obj.js';
19
9
 
20
- const { Fail } = assert;
10
+ /**
11
+ * @import {EReturn} from '@endo/far';
12
+ */
21
13
 
22
14
  export const DEFAULT_CBUF_SIZE = 100 * 1024 * 1024;
23
15
  export const DEFAULT_CBUF_FILE = 'flight-recorder.bin';
@@ -31,17 +23,24 @@ const I_ARENA_START = 4 * BigUint64Array.BYTES_PER_ELEMENT;
31
23
 
32
24
  const RECORD_HEADER_SIZE = BigUint64Array.BYTES_PER_ELEMENT;
33
25
 
26
+ /**
27
+ * Initializes a circular buffer with the given size, creating the buffer file if it doesn't exist or is not large enough.
28
+ *
29
+ * @param {string} bufferFile - the file path for the circular buffer
30
+ * @param {number} circularBufferSize - the size of the circular buffer
31
+ * @returns {Promise<bigint>} the size of the initialized circular buffer
32
+ */
34
33
  const initializeCircularBuffer = async (bufferFile, circularBufferSize) => {
35
- if (!circularBufferSize) {
36
- return undefined;
37
- }
38
34
  // If the file doesn't exist, or is not large enough, create it.
39
- const stbuf = await fsPromises.stat(bufferFile).catch(e => {
35
+ const stbuf = await fsp.stat(bufferFile).catch(e => {
40
36
  if (e.code === 'ENOENT') {
41
37
  return undefined;
42
38
  }
43
39
  throw e;
44
40
  });
41
+
42
+ // Use the default size if not provided and file doesn't exist.
43
+ circularBufferSize = circularBufferSize || stbuf?.size || DEFAULT_CBUF_SIZE;
45
44
  const arenaSize = BigInt(circularBufferSize - I_ARENA_START);
46
45
 
47
46
  if (stbuf && stbuf.size >= I_ARENA_START) {
@@ -57,8 +56,8 @@ const initializeCircularBuffer = async (bufferFile, circularBufferSize) => {
57
56
  header.setBigUint64(I_CIRC_START, 0n);
58
57
  header.setBigUint64(I_CIRC_END, 0n);
59
58
 
60
- await fsPromises.mkdir(path.dirname(bufferFile), { recursive: true });
61
- await fsPromises.writeFile(bufferFile, headerBuf);
59
+ await fsp.mkdir(path.dirname(bufferFile), { recursive: true });
60
+ await fsp.writeFile(bufferFile, headerBuf);
62
61
 
63
62
  if (stbuf && stbuf.size >= circularBufferSize) {
64
63
  // File is big enough.
@@ -66,51 +65,21 @@ const initializeCircularBuffer = async (bufferFile, circularBufferSize) => {
66
65
  }
67
66
 
68
67
  // Increase the file size.
69
- await fsPromises.truncate(bufferFile, circularBufferSize);
68
+ await fsp.truncate(bufferFile, circularBufferSize);
70
69
  return arenaSize;
71
70
  };
72
71
 
73
- export const makeMemoryMappedCircularBuffer = async ({
74
- circularBufferSize = DEFAULT_CBUF_SIZE,
75
- stateDir = '/tmp',
76
- circularBufferFilename,
77
- }) => {
78
- const filename = circularBufferFilename || `${stateDir}/${DEFAULT_CBUF_FILE}`;
79
- // console.log({ circularBufferFilename, filename });
80
-
81
- const newArenaSize = await initializeCircularBuffer(
82
- filename,
83
- circularBufferSize,
84
- );
85
-
86
- /**
87
- * @type {Uint8Array}
88
- * BufferFromFile mmap()s the file into the process address space.
89
- */
90
- const fileBuf = BufferFromFile(filename).Uint8Array();
91
- const header = new DataView(fileBuf.buffer, 0, I_ARENA_START);
92
-
93
- // Detect the arena size from the header, if not initialized.
94
- const hdrArenaSize = header.getBigUint64(I_ARENA_SIZE);
95
- const arenaSize = newArenaSize || hdrArenaSize;
96
-
97
- const hdrMagic = header.getBigUint64(I_MAGIC);
98
- SLOG_MAGIC === hdrMagic ||
99
- Fail`${filename} is not a slog buffer; wanted magic ${SLOG_MAGIC}, got ${hdrMagic}`;
100
- arenaSize === hdrArenaSize ||
101
- Fail`${filename} arena size mismatch; wanted ${arenaSize}, got ${hdrArenaSize}`;
102
- const arena = new Uint8Array(
103
- fileBuf.buffer,
104
- header.byteLength,
105
- Number(arenaSize),
106
- );
107
-
108
- /**
109
- * @param {Uint8Array} outbuf
110
- * @param {number} [offset] offset relative to the current trailing edge (circStart) of the data
111
- * @returns {IteratorResult<Uint8Array, void>}
112
- */
113
- const readCircBuf = (outbuf, offset = 0) => {
72
+ /** @typedef {EReturn<typeof makeSimpleCircularBuffer>} CircularBuffer */
73
+
74
+ /**
75
+ *
76
+ * @param {bigint} arenaSize
77
+ * @param {DataView} header
78
+ * @param {(outbuf: Uint8Array, readStart: number, firstReadLength: number) => void} readRecord
79
+ * @param {(record: Uint8Array, firstWriteLength: number, circEnd: bigint) => Promise<void>} writeRecord
80
+ */
81
+ function makeCircBufMethods(arenaSize, header, readRecord, writeRecord) {
82
+ const readCircBuf = async (outbuf, offset = 0) => {
114
83
  offset + outbuf.byteLength <= arenaSize ||
115
84
  Fail`Reading past end of circular buffer`;
116
85
 
@@ -132,19 +101,13 @@ export const makeMemoryMappedCircularBuffer = async ({
132
101
  // The data is contiguous, like ---AAABBB---
133
102
  return { done: true, value: undefined };
134
103
  }
135
- outbuf.set(arena.subarray(readStart, readStart + firstReadLength));
136
- if (firstReadLength < outbuf.byteLength) {
137
- outbuf.set(
138
- arena.subarray(0, outbuf.byteLength - firstReadLength),
139
- firstReadLength,
140
- );
141
- }
104
+ await readRecord(outbuf, readStart, firstReadLength);
142
105
  return { done: false, value: outbuf };
143
106
  };
144
107
 
145
- /** @param {Uint8Array} data */
146
- const writeCircBuf = data => {
147
- if (RECORD_HEADER_SIZE + data.byteLength > arena.byteLength) {
108
+ /** @type {(data: Uint8Array) => Promise<void>} */
109
+ const writeCircBuf = async data => {
110
+ if (RECORD_HEADER_SIZE + data.byteLength > arenaSize) {
148
111
  // The data is too big to fit in the arena, so skip it.
149
112
  const tooBigRecord = JSON.stringify({
150
113
  type: 'slog-record-too-big',
@@ -153,14 +116,17 @@ export const makeMemoryMappedCircularBuffer = async ({
153
116
  data = new TextEncoder().encode(tooBigRecord);
154
117
  }
155
118
 
156
- if (RECORD_HEADER_SIZE + data.byteLength > arena.byteLength) {
119
+ if (RECORD_HEADER_SIZE + data.byteLength > arenaSize) {
157
120
  // Silently drop, it just doesn't fit.
158
121
  return;
159
122
  }
160
123
 
124
+ // Allocate for the data and a header
161
125
  const record = new Uint8Array(RECORD_HEADER_SIZE + data.byteLength);
126
+ // Set the data, after the header
162
127
  record.set(data, RECORD_HEADER_SIZE);
163
128
 
129
+ // Set the size in the header
164
130
  const lengthPrefix = new DataView(record.buffer);
165
131
  lengthPrefix.setBigUint64(0, BigInt(data.byteLength));
166
132
 
@@ -179,9 +145,10 @@ export const makeMemoryMappedCircularBuffer = async ({
179
145
 
180
146
  // Advance the start pointer until we have space to write the record.
181
147
  let overlap = BigInt(record.byteLength) - capacity;
148
+ await null;
182
149
  while (overlap > 0n) {
183
150
  const startRecordLength = new Uint8Array(RECORD_HEADER_SIZE);
184
- const { done } = readCircBuf(startRecordLength);
151
+ const { done } = await readCircBuf(startRecordLength);
185
152
  if (done) {
186
153
  break;
187
154
  }
@@ -206,31 +173,147 @@ export const makeMemoryMappedCircularBuffer = async ({
206
173
  );
207
174
  }
208
175
 
209
- arena.set(record.subarray(0, firstWriteLength), Number(circEnd));
210
- if (firstWriteLength < record.byteLength) {
211
- // Write to the beginning of the arena.
212
- arena.set(record.subarray(firstWriteLength, record.byteLength), 0);
213
- }
214
176
  header.setBigUint64(
215
177
  I_CIRC_END,
216
178
  (circEnd + BigInt(record.byteLength)) % arenaSize,
217
179
  );
180
+
181
+ return writeRecord(record, firstWriteLength, circEnd);
218
182
  };
219
183
 
220
- const writeJSON = (obj, jsonObj = serializeSlogObj(obj)) => {
221
- // Prepend a newline so that the file can be more easily manipulated.
222
- const data = new TextEncoder().encode(`\n${jsonObj}`);
223
- // console.log('have obj', obj, data);
224
- writeCircBuf(data);
184
+ return { readCircBuf, writeCircBuf };
185
+ }
186
+
187
+ /**
188
+ * @param {{
189
+ * circularBufferSize?: number,
190
+ * stateDir?: string,
191
+ * circularBufferFilename?: string
192
+ * }} opts
193
+ */
194
+ export const makeSimpleCircularBuffer = async ({
195
+ circularBufferSize = DEFAULT_CBUF_SIZE,
196
+ stateDir = '/tmp',
197
+ circularBufferFilename,
198
+ }) => {
199
+ const filename = circularBufferFilename || `${stateDir}/${DEFAULT_CBUF_FILE}`;
200
+
201
+ const newArenaSize = await initializeCircularBuffer(
202
+ filename,
203
+ circularBufferSize,
204
+ );
205
+
206
+ const file = await fsp.open(filename, 'r+');
207
+
208
+ const headerBuffer = Buffer.alloc(I_ARENA_START);
209
+
210
+ await file.read({
211
+ buffer: headerBuffer,
212
+ length: I_ARENA_START,
213
+ position: 0,
214
+ });
215
+ const header = new DataView(headerBuffer.buffer);
216
+
217
+ // Detect the arena size from the header, if not initialized.
218
+ const hdrArenaSize = header.getBigUint64(I_ARENA_SIZE);
219
+ const arenaSize = newArenaSize || hdrArenaSize;
220
+
221
+ const hdrMagic = header.getBigUint64(I_MAGIC);
222
+ SLOG_MAGIC === hdrMagic ||
223
+ Fail`${filename} is not a slog buffer; wanted magic ${SLOG_MAGIC}, got ${hdrMagic}`;
224
+ arenaSize === hdrArenaSize ||
225
+ Fail`${filename} arena size mismatch; wanted ${arenaSize}, got ${hdrArenaSize}`;
226
+
227
+ /** @type {(outbuf: Uint8Array, readStart: number, firstReadLength: number) => Promise<void>} */
228
+ const readRecord = async (outbuf, readStart, firstReadLength) => {
229
+ const { bytesRead } = await file.read(outbuf, {
230
+ length: firstReadLength,
231
+ position: Number(readStart) + I_ARENA_START,
232
+ });
233
+ assert.equal(bytesRead, firstReadLength, 'Too few bytes read');
234
+
235
+ if (bytesRead < outbuf.byteLength) {
236
+ const length = outbuf.byteLength - firstReadLength;
237
+ const { bytesRead: bytesRead2 } = await file.read(outbuf, {
238
+ offset: firstReadLength,
239
+ length,
240
+ position: I_ARENA_START,
241
+ });
242
+ assert.equal(bytesRead2, length, 'Too few bytes read');
243
+ }
225
244
  };
226
245
 
227
- return { readCircBuf, writeCircBuf, writeJSON };
246
+ /**
247
+ * Writes to the file, offset by the header size. Also updates the file header.
248
+ *
249
+ * @param {Uint8Array} record
250
+ * @param {number} firstWriteLength
251
+ * @param {bigint} circEnd
252
+ */
253
+ const writeRecord = async (record, firstWriteLength, circEnd) => {
254
+ await file.write(
255
+ record,
256
+ // TS saying options bag not available
257
+ 0,
258
+ firstWriteLength,
259
+ I_ARENA_START + Number(circEnd),
260
+ );
261
+ if (firstWriteLength < record.byteLength) {
262
+ // Write to the beginning of the arena.
263
+ await file.write(
264
+ record,
265
+ firstWriteLength,
266
+ record.byteLength - firstWriteLength,
267
+ I_ARENA_START,
268
+ );
269
+ }
270
+
271
+ // Write out the updated file header.
272
+ // This is somewhat independent of writing the record itself, but it needs
273
+ // updating each time a record is written.
274
+ await file.write(headerBuffer, undefined, undefined, 0);
275
+ };
276
+
277
+ return {
278
+ fileHandle: file,
279
+ ...makeCircBufMethods(arenaSize, header, readRecord, writeRecord),
280
+ };
228
281
  };
229
282
 
230
- export const makeSlogSender = async opts => {
231
- const { writeJSON } = await makeMemoryMappedCircularBuffer(opts);
283
+ /**
284
+ *
285
+ * @param {Pick<CircularBuffer, 'fileHandle' | 'writeCircBuf'>} circBuf
286
+ */
287
+ export const makeSlogSenderFromBuffer = ({ fileHandle, writeCircBuf }) => {
288
+ /** @type {Promise<void> | undefined} */
289
+ let toWrite = Promise.resolve();
290
+ const writeJSON = (obj, serialized = serializeSlogObj(obj)) => {
291
+ // Prepend a newline so that the file can be more easily manipulated.
292
+ const data = new TextEncoder().encode(`\n${serialized}`);
293
+ // console.log('have obj', obj, data);
294
+ toWrite = toWrite?.then(() => writeCircBuf(data));
295
+ };
232
296
  return Object.assign(writeJSON, {
233
- forceFlush: async () => {},
297
+ forceFlush: async () => {
298
+ await toWrite;
299
+ await fileHandle.datasync();
300
+ },
301
+ shutdown: async () => {
302
+ const lastWritten = toWrite;
303
+ toWrite = undefined;
304
+ await lastWritten;
305
+ await fileHandle.close();
306
+ },
234
307
  usesJsonObject: true,
235
308
  });
236
309
  };
310
+
311
+ /**
312
+ * Loaded dynamically by makeSlogSender()
313
+ *
314
+ * @type {import('./index.js').MakeSlogSender}
315
+ */
316
+ export const makeSlogSender = async opts => {
317
+ const { fileHandle, writeCircBuf } = await makeSimpleCircularBuffer(opts);
318
+ return makeSlogSenderFromBuffer({ fileHandle, writeCircBuf });
319
+ };
@@ -1,11 +1,11 @@
1
1
  #! /usr/bin/env node
2
- /* global process */
2
+ /* eslint-env node */
3
3
  // frcat - print out the contents of a flight recorder
4
4
  // NOTE: this only works on inactive recorder files where the writer has terminated
5
5
 
6
6
  import '@endo/init';
7
7
 
8
- import { makeMemoryMappedCircularBuffer } from './flight-recorder.js';
8
+ import { makeSimpleCircularBuffer } from './flight-recorder.js';
9
9
 
10
10
  const main = async () => {
11
11
  const files = process.argv.slice(2);
@@ -14,8 +14,7 @@ const main = async () => {
14
14
  }
15
15
 
16
16
  for await (const file of files) {
17
- // eslint-disable-next-line @jessie.js/no-nested-await
18
- const { readCircBuf } = await makeMemoryMappedCircularBuffer({
17
+ const { readCircBuf } = await makeSimpleCircularBuffer({
19
18
  circularBufferFilename: file,
20
19
  circularBufferSize: 0,
21
20
  });
@@ -23,7 +22,7 @@ const main = async () => {
23
22
  let offset = 0;
24
23
  for (;;) {
25
24
  const lenBuf = new Uint8Array(BigUint64Array.BYTES_PER_ELEMENT);
26
- const { done } = readCircBuf(lenBuf, offset);
25
+ const { done } = await readCircBuf(lenBuf, offset);
27
26
  if (done) {
28
27
  break;
29
28
  }
@@ -31,7 +30,7 @@ const main = async () => {
31
30
  const dv = new DataView(lenBuf.buffer);
32
31
  const len = Number(dv.getBigUint64(0));
33
32
 
34
- const { done: done2, value: buf } = readCircBuf(
33
+ const { done: done2, value: buf } = await readCircBuf(
35
34
  new Uint8Array(len),
36
35
  offset,
37
36
  );
@@ -50,13 +49,18 @@ const main = async () => {
50
49
  }
51
50
 
52
51
  // If the buffer is full, wait for stdout to drain.
53
- // eslint-disable-next-line no-await-in-loop, @jessie.js/no-nested-await
54
52
  await new Promise(resolve => process.stdout.once('drain', resolve));
55
53
  }
56
54
  }
57
55
  };
58
56
 
59
- main().catch(err => {
60
- console.error(err);
61
- process.exitCode = 1;
62
- });
57
+ process.exitCode = 1;
58
+ main().then(
59
+ () => {
60
+ process.exitCode = 0;
61
+ },
62
+ err => {
63
+ console.error('Failed with', err);
64
+ process.exit(process.exitCode || 1);
65
+ },
66
+ );
package/src/index.js CHANGED
@@ -13,6 +13,9 @@ export * from './make-slog-sender.js';
13
13
  * shutdown?: () => Promise<void>;
14
14
  * }} SlogSender
15
15
  */
16
+ /**
17
+ * @typedef {(opts: import('./index.js').MakeSlogSenderOptions) => SlogSender | undefined} MakeSlogSender
18
+ */
16
19
  /**
17
20
  * @typedef {MakeSlogSenderCommonOptions & Record<string, unknown>} MakeSlogSenderOptions
18
21
  * @typedef {object} MakeSlogSenderCommonOptions
@@ -31,17 +34,20 @@ export const tryFlushSlogSender = async (
31
34
  slogSender,
32
35
  { env = {}, log } = {},
33
36
  ) => {
34
- await Promise.resolve(slogSender?.forceFlush?.()).catch(err => {
37
+ await null;
38
+ try {
39
+ await slogSender?.forceFlush?.();
40
+ } catch (err) {
35
41
  log?.('Failed to flush slog sender', err);
36
42
  if (err.errors) {
37
- err.errors.forEach(error => {
43
+ for (const error of err.errors) {
38
44
  log?.('nested error:', error);
39
- });
45
+ }
40
46
  }
41
47
  if (env.SLOGSENDER_FAIL_ON_ERROR) {
42
48
  throw err;
43
49
  }
44
- });
50
+ }
45
51
  };
46
52
 
47
53
  export const getResourceAttributes = ({
@@ -58,64 +64,57 @@ export const getResourceAttributes = ({
58
64
  SDK_REVISION;
59
65
  }
60
66
  if (!resourceAttributes[SemanticResourceAttributes.SERVICE_INSTANCE_ID]) {
61
- resourceAttributes[
62
- SemanticResourceAttributes.SERVICE_INSTANCE_ID
63
- ] = `${Math.random()}`;
67
+ resourceAttributes[SemanticResourceAttributes.SERVICE_INSTANCE_ID] =
68
+ `${Math.random()}`;
64
69
  }
65
70
  if (serviceName) {
66
71
  resourceAttributes[SemanticResourceAttributes.SERVICE_NAME] = serviceName;
67
72
  }
68
73
  if (OTEL_RESOURCE_ATTRIBUTES) {
69
74
  // Allow overriding resource attributes.
70
- OTEL_RESOURCE_ATTRIBUTES.split(',').forEach(kv => {
75
+ for (const kv of OTEL_RESOURCE_ATTRIBUTES.split(',')) {
71
76
  const match = kv.match(/^([^=]*)=(.*)$/);
72
77
  if (match) {
73
78
  resourceAttributes[match[1]] = match[2];
74
79
  }
75
- });
80
+ }
76
81
  }
77
82
  return resourceAttributes;
78
83
  };
79
84
 
80
85
  /**
81
86
  * @typedef {object} Powers
82
- * @property {{ warn: Console['warn'] }} console
87
+ * @property {Pick<Console, 'warn'>} console
83
88
  * @property {NodeJS.ProcessEnv} env
84
89
  * @property {import('@opentelemetry/sdk-metrics').View[]} views
85
90
  * @property {string} [serviceName]
86
91
  */
87
92
 
88
93
  /**
89
- * @param {Partial<Powers>} param0
94
+ * @param {Partial<Powers>} powers
90
95
  */
91
- const getPrometheusMeterProvider = ({
96
+ export const getPrometheusMeterProvider = ({
92
97
  console = globalThis.console,
93
98
  env = process.env,
94
99
  views,
95
100
  ...rest
96
101
  } = {}) => {
97
- const { OTEL_EXPORTER_PROMETHEUS_PORT } = env;
98
- if (!OTEL_EXPORTER_PROMETHEUS_PORT) {
99
- // No Prometheus config, so don't install.
100
- return undefined;
101
- }
102
+ const { OTEL_EXPORTER_PROMETHEUS_HOST, OTEL_EXPORTER_PROMETHEUS_PORT } = env;
103
+
104
+ // The opt-in signal is a non-empty OTEL_EXPORTER_PROMETHEUS_PORT.
105
+ if (!OTEL_EXPORTER_PROMETHEUS_PORT) return;
102
106
 
103
107
  const resource = new Resource(getResourceAttributes({ env, ...rest }));
104
108
 
105
- const port =
106
- parseInt(OTEL_EXPORTER_PROMETHEUS_PORT || '', 10) ||
107
- PrometheusExporter.DEFAULT_OPTIONS.port;
109
+ const { DEFAULT_OPTIONS } = PrometheusExporter;
110
+ const host = OTEL_EXPORTER_PROMETHEUS_HOST || DEFAULT_OPTIONS.host;
111
+ const port = +OTEL_EXPORTER_PROMETHEUS_PORT || DEFAULT_OPTIONS.port;
112
+ const url = `http://${host || '0.0.0.0'}:${port}${DEFAULT_OPTIONS.endpoint}`;
108
113
 
109
- const exporter = new PrometheusExporter(
110
- {
111
- port,
112
- },
113
- () => {
114
- console.warn(
115
- `Prometheus scrape endpoint: http://0.0.0.0:${port}${PrometheusExporter.DEFAULT_OPTIONS.endpoint}`,
116
- );
117
- },
118
- );
114
+ const options = { host, port, appendTimestamp: true };
115
+ const exporter = new PrometheusExporter(options, () => {
116
+ console.warn(`Prometheus scrape endpoint: ${url}`);
117
+ });
119
118
 
120
119
  const provider = new MeterProvider({ resource, views });
121
120
  provider.addMetricReader(exporter);
@@ -1,5 +1,5 @@
1
1
  #! /usr/bin/env node
2
- /* global setTimeout */
2
+ /* eslint-env node */
3
3
  import '@endo/init';
4
4
 
5
5
  import fs from 'fs';
@@ -11,7 +11,8 @@ import { makeSlogSender } from './make-slog-sender.js';
11
11
 
12
12
  const LINE_COUNT_TO_FLUSH = 10000;
13
13
  const ELAPSED_MS_TO_FLUSH = 3000;
14
- const MAX_LINE_COUNT_PER_PERIOD = 1000;
14
+ const MAX_LINE_COUNT_PER_PERIOD = 10000;
15
+ const MAX_BLOCKS_PER_PERIOD = 10;
15
16
  const PROCESSING_PERIOD = 1000;
16
17
 
17
18
  async function run() {
@@ -29,7 +30,7 @@ async function run() {
29
30
  return;
30
31
  }
31
32
 
32
- const [slogFile] = args;
33
+ const slogFile = args[0] === '-' ? undefined : args[0];
33
34
  const slogSender = await makeSlogSender({
34
35
  serviceName,
35
36
  stateDir: '.',
@@ -56,14 +57,21 @@ async function run() {
56
57
  const lines = readline.createInterface({ input: slogF });
57
58
  const slogFileName = slogFile || '*stdin*';
58
59
 
59
- const progressFileName = `${slogFileName}.ingest-progress`;
60
- if (!fs.existsSync(progressFileName)) {
61
- const progress = { virtualTimeOffset: 0, lastSlogTime: 0 };
62
- fs.writeFileSync(progressFileName, JSON.stringify(progress));
60
+ const progressFileName = slogFile && `${slogFileName}.ingest-progress`;
61
+ const progress = { virtualTimeOffset: 0, lastSlogTime: 0 };
62
+ if (progressFileName) {
63
+ if (!fs.existsSync(progressFileName)) {
64
+ fs.writeFileSync(progressFileName, JSON.stringify(progress));
65
+ } else {
66
+ Object.assign(
67
+ progress,
68
+ JSON.parse(fs.readFileSync(progressFileName).toString()),
69
+ );
70
+ }
63
71
  }
64
- const progress = JSON.parse(fs.readFileSync(progressFileName).toString());
65
72
 
66
73
  let linesProcessedThisPeriod = 0;
74
+ let blocksInThisPeriod = 0;
67
75
  let startOfLastPeriod = 0;
68
76
 
69
77
  let lastTime = Date.now();
@@ -74,11 +82,13 @@ async function run() {
74
82
  if (!flush) {
75
83
  return;
76
84
  }
77
- await slogSender.forceFlush();
78
- fs.writeFileSync(progressFileName, JSON.stringify(progress));
85
+ await slogSender.forceFlush?.();
86
+ if (progressFileName) {
87
+ fs.writeFileSync(progressFileName, JSON.stringify(progress));
88
+ }
79
89
  };
80
90
 
81
- console.log(`parsing`, slogFileName);
91
+ console.warn(`parsing`, slogFileName);
82
92
 
83
93
  let update = false;
84
94
  const maybeUpdateStats = async now => {
@@ -106,9 +116,14 @@ async function run() {
106
116
  continue;
107
117
  }
108
118
 
119
+ const isAfterCommit = obj.type === 'cosmic-swingset-after-commit-stats';
120
+
109
121
  // Maybe wait for the next period to process a bunch of lines.
110
122
  let maybeWait;
111
- if (linesProcessedThisPeriod >= MAX_LINE_COUNT_PER_PERIOD) {
123
+ if (
124
+ linesProcessedThisPeriod >= MAX_LINE_COUNT_PER_PERIOD ||
125
+ blocksInThisPeriod >= MAX_BLOCKS_PER_PERIOD
126
+ ) {
112
127
  const delayMS = PROCESSING_PERIOD - (now - startOfLastPeriod);
113
128
  maybeWait = new Promise(resolve => setTimeout(resolve, delayMS));
114
129
  }
@@ -118,8 +133,8 @@ async function run() {
118
133
  if (now - startOfLastPeriod >= PROCESSING_PERIOD) {
119
134
  startOfLastPeriod = now;
120
135
  linesProcessedThisPeriod = 0;
136
+ blocksInThisPeriod = 0;
121
137
  }
122
- linesProcessedThisPeriod += 1;
123
138
 
124
139
  if (progress.virtualTimeOffset) {
125
140
  const virtualTime = obj.time + progress.virtualTimeOffset;
@@ -133,14 +148,30 @@ async function run() {
133
148
  // Use the original.
134
149
  slogSender(obj);
135
150
  }
151
+
152
+ linesProcessedThisPeriod += 1;
153
+ if (isAfterCommit) {
154
+ blocksInThisPeriod += 1;
155
+ lastTime = Date.now();
156
+ await stats(true);
157
+ }
136
158
  }
137
159
 
138
160
  await stats(true);
139
- console.log(
161
+ console.warn(
140
162
  `done parsing`,
141
163
  slogFileName,
142
164
  `(${lineCount} lines, ${byteCount} bytes)`,
143
165
  );
144
166
  }
145
167
 
146
- run().catch(err => console.log('err', err));
168
+ process.exitCode = 1;
169
+ run().then(
170
+ () => {
171
+ process.exitCode = 0;
172
+ },
173
+ err => {
174
+ console.error('Failed with', err);
175
+ process.exit(process.exitCode || 1);
176
+ },
177
+ );