UNPKG

@hyperlane-xyz/core

Version:

Core solidity contracts for Hyperlane

306 lines (243 loc) 6.66 kB
import { performance } from 'perf_hooks' import { Mutex } from 'async-mutex' import { sleep } from '@eth-optimism/core-utils' import { sanitizeForMetrics, benchDurationsSummary, successfulBenchRunsTotal, failedActorRunsTotal, successfulActorRunsTotal, failedBenchRunsTotal, } from './metrics' import { ActorLogger, WorkerLogger } from './logger' // eslint-disable-next-line @typescript-eslint/no-empty-function const asyncNoop = async () => {} export type AsyncCB = () => Promise<void> export interface Bencher { bench: (name: string, cb: () => Promise<any>) => Promise<any> } export type RunCB<C> = ( b: Bencher, ctx: C, logger: WorkerLogger ) => Promise<void> export interface RunOpts { runs: number | null runFor: number | null concurrency: number thinkTime: number } class Latch { private n: number private p: Promise<void> private resolver: () => void constructor(n: number) { this.n = n this.p = new Promise((resolve) => { this.resolver = resolve }) } countDown() { this.n-- if (this.n === 0) { this.resolver() } } wait() { return this.p } } export class Runner { private readonly workerId: number private readonly actor: Actor private readonly mtx: Mutex private readonly readyLatch: Latch private readonly stepper: Bencher private readonly logger: WorkerLogger constructor(workerId: number, actor: Actor, mtx: Mutex, readyLatch: Latch) { this.workerId = workerId this.actor = actor this.mtx = mtx this.readyLatch = readyLatch this.stepper = { bench: this.bench, } this.logger = new WorkerLogger(this.actor.name, workerId) } bench = async (name: string, cb: () => Promise<void>) => { const metricLabels = { actor_name: sanitizeForMetrics(this.actor.name), bench_name: sanitizeForMetrics(name), } const start = performance.now() let res try { res = await cb() } catch (e) { failedBenchRunsTotal.inc({ ...metricLabels, worker_id: this.workerId, }) throw e } benchDurationsSummary.observe(metricLabels, performance.now() - start) successfulBenchRunsTotal.inc({ ...metricLabels, worker_id: this.workerId, }) return res } async run(opts: RunOpts) { const actor = this.actor this.logger.log('Setting up.') let ctx try { ctx = await this.mtx.runExclusive(this.actor.setupRun) } finally { this.readyLatch.countDown() } this.logger.log('Waiting for other workers to finish setup.') await this.readyLatch.wait() this.logger.log('Executing.') const benchStart = performance.now() let lastDurPrint = benchStart let i = 0 const metricLabels = { actor_name: sanitizeForMetrics(this.actor.name), } while (true) { const now = performance.now() if ( (opts.runs && i === opts.runs) || (opts.runFor && now - benchStart >= opts.runFor) ) { this.logger.log(`Worker exited.`) break } try { await this.actor.run(this.stepper, ctx, this.logger) } catch (e) { console.error('Error in actor run:') console.error(`Benchmark name: ${actor.name}`) console.error(`Worker ID: ${this.workerId}`) console.error(`Run index: ${i}`) console.error('Stack trace:') console.error(e) failedActorRunsTotal.inc(metricLabels) await sleep(1000) continue } successfulActorRunsTotal.inc(metricLabels) i++ if ( (opts.runs && (i % 10 === 0 || i === opts.runs)) || now - lastDurPrint > 10000 ) { this.logger.log(`Completed run ${i} of ${opts.runs}.`) } if (opts.runFor && now - lastDurPrint > 10000) { const runningFor = Math.floor(now - benchStart) this.logger.log(`Running for ${runningFor} of ${opts.runFor} ms.`) lastDurPrint = now } if (opts.thinkTime > 0) { await sleep(opts.thinkTime) } } await this.mtx.runExclusive(() => this.actor.tearDownRun(ctx)) } } export class Actor { public readonly name: string private _setupEnv: AsyncCB = asyncNoop private _tearDownEnv: AsyncCB = asyncNoop private _setupRun: <C>() => Promise<C> = asyncNoop as any private _tearDownRun: <C>(ctx: C) => Promise<void> = asyncNoop as any // eslint-disable-next-line @typescript-eslint/no-empty-function private _run: <C>(b: Bencher, ctx: C, logger: WorkerLogger) => Promise<void> = asyncNoop private logger: ActorLogger constructor(name: string) { this.name = name this.logger = new ActorLogger(this.name) } get setupEnv(): AsyncCB { return this._setupEnv } set setupEnv(value: AsyncCB) { this._setupEnv = value } get tearDownEnv(): AsyncCB { return this._tearDownEnv } set tearDownEnv(value: AsyncCB) { this._tearDownEnv = value } get setupRun(): <C>() => Promise<C> { return this._setupRun } set setupRun(value: () => Promise<any>) { this._setupRun = value } get tearDownRun(): <C>(ctx: C) => Promise<void> { return this._tearDownRun } set tearDownRun(value: (ctx: any) => Promise<any>) { this._tearDownRun = value } get run(): RunCB<any> { return this._run } set run(cb: RunCB<any>) { this._run = cb } async exec(opts: RunOpts) { this.logger.log('Setting up.') try { await this.setupEnv() } catch (e) { console.error(`Error in setupEnv hook for actor "${this.name}":`) console.error(e) return } this.logger.log('Starting.') const parallelRuns = [] const mtx = new Mutex() const latch = new Latch(opts.concurrency) for (let i = 0; i < opts.concurrency; i++) { const runner = new Runner(i, this, mtx, latch) parallelRuns.push(runner.run(opts)) } await Promise.all(parallelRuns) this.logger.log('Tearing down.') try { await this.tearDownEnv() } catch (e) { console.error(`Error in after hook for benchmark "${this.name}":`) console.error(e) return } this.logger.log('Teardown complete.') } } export class Runtime { private actors: Actor[] = [] addActor(actor: Actor) { this.actors.push(actor) } async run(opts: Partial<RunOpts>) { opts = { runs: 1, concurrency: 1, runFor: null, thinkTime: 0, ...(opts || {}), } if (opts.runFor) { opts.runs = null } for (const actor of this.actors) { await actor.exec(opts as RunOpts) } } }