Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
73dd95b
docs: add CLAUDE.md for Claude Code guidance
mcollina Sep 28, 2025
2cf3295
feat: migrate test suite from Jest to Node.js built-in test runner
mcollina Sep 28, 2025
3108ad2
fix: resolve test failures after Jest to node:test migration
mcollina Oct 6, 2025
abdbda9
feat: vendor tdigest and bintrees dependencies
mcollina Oct 6, 2025
755e146
perf: optimize tdigest by replacing forEach/map with for loops
mcollina Oct 6, 2025
bcbdaf3
Switch findBound function to a binary search
mcollina Oct 13, 2025
77fa56e
Optimize keyFrom
mcollina Oct 13, 2025
6d8eb17
optimize keyFrom
mcollina Oct 13, 2025
c73ea76
refactor: use async fs/promises in osMemoryHeapLinux
mcollina Oct 14, 2025
125404b
refactor: add concurrency control and promise-based collection to osM…
mcollina Oct 14, 2025
5c34897
perf: eliminate duplicate sorting in metric creation
mcollina Nov 6, 2025
0e2bb32
fix: skip Linux-only test on non-Linux platforms
mcollina Nov 7, 2025
ea63574
perf: optimize histogram and string escaping for better metrics seria…
mcollina Nov 9, 2025
24a18db
perf: replace .map() with for loop in registry.metrics()
mcollina Nov 17, 2025
a51e465
perf: avoid array conversion in getMetricsAsJSON
mcollina Nov 17, 2025
60e29f5
fix: resolve linting errors in test files
mcollina Nov 17, 2025
5251e50
perf: eliminate unnecessary promise allocation in metrics and registr…
mcollina Nov 17, 2025
4d589c6
removed TODO
mcollina Nov 17, 2025
480f0ca
docs: update CHANGELOG.md with recent performance improvements and te…
mcollina Nov 17, 2025
176df08
perf: optimize validate() and keyFrom() for metrics without labels
mcollina Nov 17, 2025
e25e777
Merge branch 'main' into perf-improvements
mcollina Nov 19, 2025
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
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ project adheres to [Semantic Versioning](http://semver.org/).
- Drop support for Node.js versions 16, 18, 21 and 23
- Metric internal storage ('hashMap') changed to a separate object, LabelMap. If you have
subclassed the built-in metric types you may need to adjust your code.
- Migrated test suite from Jest to Node.js built-in test runner (node:test)

### Changed

Expand All @@ -27,10 +28,31 @@ project adheres to [Semantic Versioning](http://semver.org/).
- perf: New, more space-efficient storage engine, 20-45% faster stats recording
- perf: Further improvement to key generation cost
- fix: Browser compatibility for Gauge.startTimer()
- perf: Eliminate unnecessary promise allocation in metrics and registry when collect functions are not present
- Split get() methods into sync and async versions to avoid promise overhead
- Registry methods (metrics(), getMetricsAsString(), getMetricsAsJSON()) now return synchronously when possible
- Significant performance improvements: up to 38% faster registry serialization, 107% faster histogram operations, 123% faster counter/gauge with labels
- perf: Avoid array conversion in getMetricsAsJSON by directly iterating over metric values (~1.3% faster)
- perf: Replace .map() with for loops in registry.metrics() for consistency with codebase optimization patterns
- perf: Optimize histogram and string escaping for better metrics serialization
- Replace .map() with for loops in histogram operations
- Inline extractBucketValuesForExport() to eliminate function call overhead
- Refactor escapeLabelValue() and escapeString() to single-pass traversal (~4% improvement)
- perf: Eliminate duplicate sorting in metric creation by passing pre-sorted labelNames to LabelMap (~19% improvement)
- perf: Optimize keyFrom function for better label hashing performance
- perf: Switch findBound function to binary search in histogram implementation
- perf: Optimize tdigest by replacing forEach/map with for loops (~25% faster percentile queries)
- refactor: Use async fs/promises in osMemoryHeapLinux instead of synchronous readFileSync
- refactor: Add concurrency control and promise-based collection to osMemoryHeapLinux
- fix: Skip Linux-only processOpenFileDescriptors test on non-Linux platforms
- fix: Resolve linting errors in test files (regex escaping, JSDoc formatting)
- fix: Resolve test failures after Jest to node:test migration (TypeError expectations, deepStrictEqual comparisons)

### Added

- Expanded benchmarking code
- feat: Vendor tdigest@0.1.1 and bintrees dependencies to eliminate external dependency on unmaintained packages
- docs: Add CLAUDE.md for Claude Code guidance with comprehensive development commands and architecture overview

## [15.1.3] - 2024-06-27

Expand Down
84 changes: 84 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

This is **prom-client**, a Prometheus client library for Node.js that provides metrics collection and exposure functionality. It supports all standard Prometheus metric types: counters, gauges, histograms, and summaries.

## Development Commands

### Essential Commands

- `npm test` - Full test suite (lint + prettier + typescript + unit tests with coverage)
- `npm run test-unit` - Run node:test unit tests only
- `npm run lint` - ESLint validation
- `npm run check-prettier` - Prettier formatting check
- `npm run compile-typescript` - TypeScript compilation check

### Running Single Tests

Tests are located in `/test/` and follow the pattern `*Test.js`. Use node:test directly:

```bash
node --test test/counterTest.js
node --test test/metrics/
node --test "test/**/*Test.js"
```

## Code Architecture

### Core Structure

The library is organized around four main metric types in `/lib/`:

- **counter.js** - Cumulative metrics that only increase
- **gauge.js** - Metrics that can go up and down
- **histogram.js** - Samples observations in configurable buckets
- **summary.js** - Calculates percentiles of observed values

### Key Components

- **registry.js** - Central registry for all metrics, supports Prometheus and OpenMetrics formats
- **defaultMetrics.js** - Orchestrates collection of Node.js system metrics (CPU, memory, GC, etc.)
- **cluster.js** - Aggregator registry for Node.js cluster support
- **pushgateway.js** - Push metrics to Prometheus Pushgateway

### Entry Points

- **index.js** - Main entry point exporting all public APIs
- **index.d.ts** - Comprehensive TypeScript definitions

### Default Metrics (/lib/metrics/)

System metrics are modular and platform-aware:

- Some metrics (like file descriptors) are Linux-only
- Event loop lag, garbage collection, heap usage for Node.js internals
- Process information and resource usage

## Development Patterns

### Metric Creation Pattern

Each metric type follows a consistent pattern:

1. Extends base `Metric` class
2. Implements `collect()` method returning metric samples
3. Supports labels for dimensional metrics
4. Registry handles serialization to Prometheus format

### Testing Approach

- Unit tests in `/test/` with Node.js built-in test runner (node:test)
- Metrics tests in `/test/metrics/` for default metrics
- Examples in `/example/` demonstrate real usage patterns
- Mock HTTP requests with `nock` library
- Timer mocking with `@sinonjs/fake-timers`
- Test helpers and utilities in `/test/helpers.js`

### TypeScript Support

- Full type definitions maintained alongside JS code
- `noEmit: true` - types only, no compilation to JS
- Strict mode enabled for type safety
226 changes: 226 additions & 0 deletions benchmarks/bintrees.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import { group, bench, run } from 'mitata';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I had too many other PRs outstanding so I didn’t post this yet, but I have a PR to replace benchmark-regressions with a workalike that wraps bench-node, which is maintained by a NodeJS core contributor. If I can get him to land one more PR I can clean up some junk in the output and then file that here, clearing the dead deps.

import RBTree from '../lib/bintrees/rbtree.js';

// Comparator function for numbers
const compareNumbers = (a, b) => a - b;

// Comparator function for objects with 'value' property
const compareObjects = (a, b) => a.value - b.value;

group('RBTree insert operations', () => {
bench('insert 10 sequential values', () => {
const tree = new RBTree(compareNumbers);
for (let i = 0; i < 10; i++) {
tree.insert(i);
}
});

bench('insert 100 sequential values', () => {
const tree = new RBTree(compareNumbers);
for (let i = 0; i < 100; i++) {
tree.insert(i);
}
});

bench('insert 1000 sequential values', () => {
const tree = new RBTree(compareNumbers);
for (let i = 0; i < 1000; i++) {
tree.insert(i);
}
});

bench('insert 100 random values', () => {
const tree = new RBTree(compareNumbers);
for (let i = 0; i < 100; i++) {
tree.insert(Math.random() * 10000);
}
});

bench('insert 1000 random values', () => {
const tree = new RBTree(compareNumbers);
for (let i = 0; i < 1000; i++) {
tree.insert(Math.random() * 10000);
}
});

bench('insert 100 reverse sequential values', () => {
const tree = new RBTree(compareNumbers);
for (let i = 99; i >= 0; i--) {
tree.insert(i);
}
});
});

group('RBTree find operations', () => {
const tree100 = new RBTree(compareNumbers);
for (let i = 0; i < 100; i++) {
tree100.insert(i);
}

const tree1000 = new RBTree(compareNumbers);
for (let i = 0; i < 1000; i++) {
tree1000.insert(i);
}

const tree10000 = new RBTree(compareNumbers);
for (let i = 0; i < 10000; i++) {
tree10000.insert(i);
}

bench('find in tree with 100 values', () => {
tree100.find(50);
});

bench('find in tree with 1000 values', () => {
tree1000.find(500);
});

bench('find in tree with 10000 values', () => {
tree10000.find(5000);
});

bench('find non-existent in tree with 1000 values', () => {
tree1000.find(-1);
});
});

group('RBTree min/max operations', () => {
const tree100 = new RBTree(compareNumbers);
for (let i = 0; i < 100; i++) {
tree100.insert(Math.random() * 1000);
}

const tree1000 = new RBTree(compareNumbers);
for (let i = 0; i < 1000; i++) {
tree1000.insert(Math.random() * 1000);
}

bench('min with 100 values', () => {
tree100.min();
});

bench('max with 100 values', () => {
tree100.max();
});

bench('min with 1000 values', () => {
tree1000.min();
});

bench('max with 1000 values', () => {
tree1000.max();
});
});

group('RBTree iteration operations', () => {
const tree100 = new RBTree(compareNumbers);
for (let i = 0; i < 100; i++) {
tree100.insert(Math.random() * 1000);
}

const tree1000 = new RBTree(compareNumbers);
for (let i = 0; i < 1000; i++) {
tree1000.insert(Math.random() * 1000);
}

bench('iterate all 100 values with each()', () => {
tree100.each(() => {});
});

bench('iterate all 1000 values with each()', () => {
tree1000.each(() => {});
});

bench('iterate 10 values with iterator', () => {
const iter = tree1000.iterator();
for (let i = 0; i < 10; i++) {
iter.next();
}
});
});

group('RBTree lowerBound/upperBound operations', () => {
const tree = new RBTree(compareNumbers);
for (let i = 0; i < 1000; i++) {
tree.insert(i * 2); // Even numbers only
}

bench('lowerBound exact match', () => {
tree.lowerBound(500);
});

bench('lowerBound between values', () => {
tree.lowerBound(501);
});

bench('upperBound', () => {
tree.upperBound(500);
});
});

group('RBTree remove operations', () => {
bench('insert and remove 100 values', () => {
const tree = new RBTree(compareNumbers);
for (let i = 0; i < 100; i++) {
tree.insert(i);
}
for (let i = 0; i < 100; i++) {
tree.remove(i);
}
});

bench('insert 100, remove 50', () => {
const tree = new RBTree(compareNumbers);
for (let i = 0; i < 100; i++) {
tree.insert(i);
}
for (let i = 0; i < 50; i++) {
tree.remove(i);
}
});

bench('remove from middle', () => {
const tree = new RBTree(compareNumbers);
for (let i = 0; i < 100; i++) {
tree.insert(i);
}
tree.remove(50);
});
});

group('RBTree with complex objects', () => {
bench('insert 100 objects', () => {
const tree = new RBTree(compareObjects);
for (let i = 0; i < 100; i++) {
tree.insert({ value: i, data: `item${i}` });
}
});

bench('find in tree with 100 objects', () => {
const tree = new RBTree(compareObjects);
for (let i = 0; i < 100; i++) {
tree.insert({ value: i, data: `item${i}` });
}
tree.find({ value: 50 });
});
});

group('RBTree clear operation', () => {
bench('clear tree with 100 values', () => {
const tree = new RBTree(compareNumbers);
for (let i = 0; i < 100; i++) {
tree.insert(i);
}
tree.clear();
});

bench('clear tree with 1000 values', () => {
const tree = new RBTree(compareNumbers);
for (let i = 0; i < 1000; i++) {
tree.insert(i);
}
tree.clear();
});
});

run();
Loading
Loading