Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add histogram and summary metric types #2705

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/interface-compliance-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,12 @@
"p-wait-for": "^5.0.2",
"protons-runtime": "^5.4.0",
"sinon": "^18.0.0",
"tdigest": "^0.1.2",
"uint8arraylist": "^2.4.8",
"uint8arrays": "^5.1.0"
},
"devDependencies": {
"@types/tdigest": "^0.1.4",
"protons": "^7.5.0"
}
}
226 changes: 225 additions & 1 deletion packages/interface-compliance-tests/src/mocks/metrics.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { MultiaddrConnection, Stream, Connection, Metric, MetricGroup, StopTimer, Metrics, CalculatedMetricOptions, MetricOptions } from '@libp2p/interface'
import { TDigest } from 'tdigest'
import type { MultiaddrConnection, Stream, Connection, Metric, MetricGroup, StopTimer, Metrics, CalculatedMetricOptions, MetricOptions, Histogram, HistogramOptions, HistogramGroup, Summary, SummaryOptions, SummaryGroup, CalculatedHistogramOptions, CalculatedSummaryOptions } from '@libp2p/interface'

class DefaultMetric implements Metric {
public value: number = 0
Expand Down Expand Up @@ -68,6 +69,153 @@ class DefaultGroupMetric implements MetricGroup {
}
}

class DefaultHistogram implements Histogram {
public bucketValues = new Map<number, number>()
public countValue: number = 0
public sumValue: number = 0

constructor (opts: HistogramOptions) {
const buckets = [
...(opts.buckets ?? [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]),
Infinity
]
for (const bucket of buckets) {
this.bucketValues.set(bucket, 0)
}
}

observe (value: number): void {
this.countValue++
this.sumValue += value

for (const [bucket, count] of this.bucketValues.entries()) {
if (value <= bucket) {
this.bucketValues.set(bucket, count + 1)
}
}
}

reset (): void {
this.countValue = 0
this.sumValue = 0
for (const bucket of this.bucketValues.keys()) {
this.bucketValues.set(bucket, 0)
}
}

timer (): StopTimer {
const start = Date.now()

return () => {
this.observe(Date.now() - start)
}
}
}

class DefaultHistogramGroup implements HistogramGroup {
public histograms: Record<string, DefaultHistogram> = {}

constructor (opts: HistogramOptions) {
this.histograms = {}
}

observe (values: Partial<Record<string, number>>): void {
for (const [key, value] of Object.entries(values) as Array<[string, number]>) {
if (this.histograms[key] === undefined) {
this.histograms[key] = new DefaultHistogram({})
}

this.histograms[key].observe(value)
}
}

reset (): void {
for (const histogram of Object.values(this.histograms)) {
histogram.reset()
}
}

timer (key: string): StopTimer {
const start = Date.now()

return () => {
this.observe({ [key]: Date.now() - start })
}
}
}

class DefaultSummary implements Summary {
public sumValue: number = 0
public countValue: number = 0
public percentiles: number[]
public tdigest = new TDigest(0.01)
private readonly compressCount: number

constructor (opts: SummaryOptions) {
this.percentiles = opts.percentiles ?? [0.01, 0.05, 0.5, 0.9, 0.95, 0.99, 0.999]
this.compressCount = opts.compressCount ?? 1000
}

observe (value: number): void {
this.sumValue += value
this.countValue++

this.tdigest.push(value)
if (this.tdigest.size() > this.compressCount) {
this.tdigest.compress()
}
}

reset (): void {
this.sumValue = 0
this.countValue = 0

this.tdigest.reset()
}

timer (): StopTimer {
const start = Date.now()

return () => {
this.observe(Date.now() - start)
}
}
}

class DefaultSummaryGroup implements SummaryGroup {
public summaries: Record<string, DefaultSummary> = {}
private readonly opts: SummaryOptions

constructor (opts: SummaryOptions) {
this.summaries = {}
this.opts = opts
}

observe (values: Record<string, number>): void {
for (const [key, value] of Object.entries(values)) {
if (this.summaries[key] === undefined) {
this.summaries[key] = new DefaultSummary(this.opts)
}

this.summaries[key].observe(value)
}
}

reset (): void {
for (const summary of Object.values(this.summaries)) {
summary.reset()
}
}

timer (key: string): StopTimer {
const start = Date.now()

return () => {
this.observe({ [key]: Date.now() - start })
}
}
}

class MockMetrics implements Metrics {
public metrics = new Map<string, any>()

Expand Down Expand Up @@ -154,6 +302,82 @@ class MockMetrics implements Metrics {

return metric
}

registerHistogram (name: string, opts: CalculatedHistogramOptions): void
registerHistogram (name: string, opts?: HistogramOptions): Histogram
registerHistogram (name: string, opts: any = {}): any {
if (name == null || name.trim() === '') {
throw new Error('Metric name is required')
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use InvalidParametersError for invalid parameters


if (opts?.calculate != null) {
// calculated metric
this.metrics.set(name, opts.calculate)
return
}

const metric = new DefaultHistogram(opts)
this.metrics.set(name, metric)

return metric
}

registerHistogramGroup (name: string, opts: CalculatedHistogramOptions<Record<string, number>>): void
registerHistogramGroup (name: string, opts?: HistogramOptions): HistogramGroup
registerHistogramGroup (name: string, opts: any = {}): any {
if (name == null || name.trim() === '') {
throw new Error('Metric name is required')
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use InvalidParametersError for invalid parameters


if (opts?.calculate != null) {
// calculated metric
this.metrics.set(name, opts.calculate)
return
}

const metric = new DefaultHistogramGroup(opts)
this.metrics.set(name, metric)

return metric
}

registerSummary (name: string, opts: CalculatedSummaryOptions): void
registerSummary (name: string, opts?: SummaryOptions): Summary
registerSummary (name: string, opts: any = {}): any {
if (name == null || name.trim() === '') {
throw new Error('Metric name is required')
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use InvalidParametersError for invalid parameters


if (opts?.calculate != null) {
// calculated metric
this.metrics.set(name, opts.calculate)
return
}

const metric = new DefaultSummary(opts)
this.metrics.set(name, metric)

return metric
}

registerSummaryGroup (name: string, opts: CalculatedSummaryOptions<Record<string, number>>): void
registerSummaryGroup (name: string, opts?: SummaryOptions): SummaryGroup
registerSummaryGroup (name: string, opts: any = {}): any {
if (name == null || name.trim() === '') {
throw new Error('Metric name is required')
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use InvalidParametersError for invalid parameters


if (opts?.calculate != null) {
// calculated metric
this.metrics.set(name, opts.calculate)
return
}

const metric = new DefaultSummaryGroup(opts)
this.metrics.set(name, metric)

return metric
}
}

export function mockMetrics (): () => Metrics {
Expand Down
Loading
Loading