@naturalcycles/nodejs-lib
Version:
Standard library for Node.js
149 lines (127 loc) • 3.73 kB
text/typescript
import { SimpleMovingAverage } from '@naturalcycles/js-lib'
import { dayjs, since } from '@naturalcycles/time-lib'
import { Transform } from 'stream'
import { inspect } from 'util'
import { boldWhite, dimGrey, mb, white, yellow } from '../..'
import { TransformOpt, TransformTyped } from '../stream.model'
export interface TransformLogProgressOptions<IN = any> extends TransformOpt {
/**
* Progress metric
* @default `progress`
*/
metric?: string
/**
* Include `heapUsed` in log.
* @default true
*/
heapUsed?: boolean
/**
* Include `heapTotal` in log.
* @default false
*/
heapTotal?: boolean
/**
* Include `rss` in log.
* @default true
*/
rss?: boolean
/**
* Log "rows per second"
* @default true
*/
logRPS?: boolean
/**
* @default true
* Set to false to disable logging progress
*/
logProgress?: boolean
/**
* Log progress event Nth record that is _processed_ (went through mapper).
* @default 100
*/
logEvery?: number
/**
* Function to return extra properties to the "progress object".
*/
extra?: (chunk: IN, index: number) => object
}
const inspectOpt: NodeJS.InspectOptions = {
colors: true,
breakLength: 100,
}
/**
* Pass-through transform that optionally logs progress.
*/
export function transformLogProgress<IN = any>(
opt: TransformLogProgressOptions = {},
): TransformTyped<IN, IN> {
const { metric = 'progress', heapTotal: logHeapTotal = false, logEvery = 100, extra } = opt
const logProgress = opt.logProgress !== false // true by default
const logHeapUsed = opt.heapUsed !== false // true by default
const logRss = opt.rss !== false // true by default
const logRPS = opt.logRPS !== false // true by default
const logEvery10 = logEvery * 10
const started = Date.now()
let lastSecondStarted = Date.now()
const sma = new SimpleMovingAverage(10) // over last 10 seconds
let processedLastSecond = 0
let progress = 0
logStats() // initial
return new Transform({
objectMode: true,
...opt,
transform(chunk: IN, _encoding, cb) {
progress++
processedLastSecond++
if (logProgress && progress % logEvery === 0) {
logStats(chunk, false, progress % logEvery10 === 0)
}
cb(null, chunk) // pass-through
},
final(cb) {
logStats(undefined, true)
cb()
},
})
function logStats(chunk?: IN, final = false, tenx = false): void {
if (!logProgress) return
const { rss, heapUsed, heapTotal } = process.memoryUsage()
const now = Date.now()
const lastRPS = processedLastSecond / ((now - lastSecondStarted) / 1000) || 0
const rpsTotal = Math.round(progress / ((now - started) / 1000)) || 0
lastSecondStarted = now
processedLastSecond = 0
const rps10 = Math.round(sma.push(lastRPS))
console.log(
inspect(
{
[metric]: progress,
...(extra && !final ? extra(chunk, progress) : {}),
...(logHeapUsed ? { heapUsed: mb(heapUsed) } : {}),
...(logHeapTotal ? { heapTotal: mb(heapTotal) } : {}),
...(logRss ? { rss: mb(rss) } : {}),
...(logRPS
? {
rps10,
rpsTotal,
}
: {}),
},
inspectOpt,
),
)
if (tenx) {
console.log(
`${dimGrey(dayjs().toPretty())} ${white(metric)} took ${yellow(
since(started),
)} so far to process ${yellow(progress)} rows`,
)
} else if (final) {
console.log(
`${boldWhite(metric)} took ${yellow(since(started))} to process ${yellow(
progress,
)} rows with total RPS of ${yellow(rpsTotal)}`,
)
}
}
}