langgraph-api 0.4.36__tar.gz → 0.4.38__tar.gz
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.
Potentially problematic release.
This version of langgraph-api might be problematic. Click here for more details.
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/PKG-INFO +1 -1
- langgraph_api-0.4.38/benchmark/Makefile +54 -0
- langgraph_api-0.4.38/benchmark/capacity_k6.js +221 -0
- langgraph_api-0.4.38/benchmark/capacity_runner.mjs +307 -0
- langgraph_api-0.4.38/benchmark/reporting/dd_reporting.py +85 -0
- langgraph_api-0.4.38/langgraph_api/__init__.py +1 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/api/meta.py +2 -1
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/cli.py +2 -2
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/config.py +25 -11
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/graph.py +1 -1
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/http_metrics.py +1 -1
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/logging.py +24 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/metadata.py +5 -5
- langgraph_api-0.4.38/langgraph_api/self_hosted_logs.py +124 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/self_hosted_metrics.py +9 -9
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/pyproject.toml +1 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/uv.lock +50 -12
- langgraph_api-0.4.36/benchmark/Makefile +0 -24
- langgraph_api-0.4.36/langgraph_api/__init__.py +0 -1
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/.gitignore +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/LICENSE +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/Makefile +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/README.md +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/benchmark/.gitignore +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/benchmark/README.md +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/benchmark/burst.js +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/benchmark/clean.js +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/benchmark/graphs.js +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/benchmark/package.json +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/benchmark/ramp.js +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/benchmark/update-revision.js +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/benchmark/weather.js +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/constraints.txt +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/forbidden.txt +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/healthcheck.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/api/__init__.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/api/a2a.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/api/assistants.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/api/mcp.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/api/openapi.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/api/runs.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/api/store.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/api/threads.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/api/ui.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/asgi_transport.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/asyncio.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/auth/__init__.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/auth/custom.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/auth/langsmith/__init__.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/auth/langsmith/backend.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/auth/langsmith/client.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/auth/middleware.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/auth/noop.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/auth/studio_user.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/command.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/cron_scheduler.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/errors.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/executor_entrypoint.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/feature_flags.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/grpc_ops/__init__.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/grpc_ops/client.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/grpc_ops/generated/__init__.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/grpc_ops/generated/core_api_pb2.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/grpc_ops/generated/core_api_pb2.pyi +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/grpc_ops/generated/core_api_pb2_grpc.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/grpc_ops/ops.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/http.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/http_metrics_utils.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/.gitignore +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/.prettierrc +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/__init__.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/base.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/build.mts +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/client.http.mts +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/client.mts +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/errors.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/global.d.ts +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/package.json +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/remote.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/schema.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/src/graph.mts +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/src/load.hooks.mjs +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/src/preload.mjs +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/src/utils/files.mts +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/src/utils/importMap.mts +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/src/utils/pythonSchemas.mts +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/src/utils/serde.mts +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/sse.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/traceblock.mts +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/tsconfig.json +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/ui.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/yarn.lock +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/middleware/__init__.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/middleware/http_logger.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/middleware/private_network.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/middleware/request_id.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/models/__init__.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/models/run.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/patch.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/queue_entrypoint.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/route.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/schema.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/serde.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/server.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/sse.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/state.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/store.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/stream.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/thread_ttl.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/traceblock.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/tunneling/cloudflare.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/utils/__init__.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/utils/cache.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/utils/config.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/utils/errors.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/utils/future.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/utils/headers.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/utils/retriable_client.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/utils/stream_codec.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/utils/uuids.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/validation.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/webhook.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/worker.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_license/__init__.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_license/validation.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_runtime/__init__.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_runtime/checkpoint.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_runtime/database.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_runtime/lifespan.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_runtime/metrics.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_runtime/ops.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_runtime/queue.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_runtime/retry.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_runtime/store.py +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/logging.json +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/openapi.json +0 -0
- {langgraph_api-0.4.36 → langgraph_api-0.4.38}/scripts/create_license.py +0 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Benchmark commands
|
|
2
|
+
BASE_URL ?= https://ifh-core-api-dr-benchmark-048d364b548f5d9790082d8ba4fb44d8.us.langgraph.app
|
|
3
|
+
RAMP_START ?= 10
|
|
4
|
+
RAMP_END ?= 1000
|
|
5
|
+
RAMP_MULTIPLIER ?= 2
|
|
6
|
+
WAIT_SECONDS ?= 60
|
|
7
|
+
SUCCESS_THRESHOLD ?= 0.99
|
|
8
|
+
CLEAR_BETWEEN_STEPS ?= true
|
|
9
|
+
CLEAR_DELAY_SECONDS ?= 5
|
|
10
|
+
DATA_SIZE ?= 1000
|
|
11
|
+
DELAY ?= 0
|
|
12
|
+
EXPAND ?= 10
|
|
13
|
+
STEPS ?= 10
|
|
14
|
+
|
|
15
|
+
benchmark-burst:
|
|
16
|
+
make benchmark-reset
|
|
17
|
+
k6 run burst.js
|
|
18
|
+
|
|
19
|
+
benchmark-ramp:
|
|
20
|
+
make benchmark-reset
|
|
21
|
+
k6 run --out json=raw_data_$(shell date +%Y-%m-%dT%H-%M-%S).json ramp.js
|
|
22
|
+
|
|
23
|
+
benchmark-capacity:
|
|
24
|
+
rm -f capacity_summary_t*.json capacity_report_*.json capacity_raw_t*.json capacity_hist_*.png capacity_pie_*.png
|
|
25
|
+
npm install
|
|
26
|
+
BASE_URL=$(BASE_URL) \
|
|
27
|
+
RAMP_START=$(RAMP_START) \
|
|
28
|
+
RAMP_END=$(RAMP_END) \
|
|
29
|
+
RAMP_MULTIPLIER=$(RAMP_MULTIPLIER) \
|
|
30
|
+
WAIT_SECONDS=$(WAIT_SECONDS) \
|
|
31
|
+
SUCCESS_THRESHOLD=$(SUCCESS_THRESHOLD) \
|
|
32
|
+
CLEAR_BETWEEN_STEPS=$(CLEAR_BETWEEN_STEPS) \
|
|
33
|
+
CLEAR_DELAY_SECONDS=$(CLEAR_DELAY_SECONDS) \
|
|
34
|
+
DATA_SIZE=$(DATA_SIZE) \
|
|
35
|
+
DELAY=$(DELAY) \
|
|
36
|
+
EXPAND=$(EXPAND) \
|
|
37
|
+
STEPS=$(STEPS) \
|
|
38
|
+
node capacity_runner.mjs
|
|
39
|
+
|
|
40
|
+
benchmark-charts:
|
|
41
|
+
npm install
|
|
42
|
+
node graphs.js $(shell ls -t raw_data_*.json | head -1) true
|
|
43
|
+
|
|
44
|
+
benchmark-reset:
|
|
45
|
+
node clean.js
|
|
46
|
+
|
|
47
|
+
benchmark-new-revision:
|
|
48
|
+
node update-revision.js
|
|
49
|
+
|
|
50
|
+
benchmark-clean:
|
|
51
|
+
rm -f results_*.json summary_*.json raw_data_*.json *_chart_*.png
|
|
52
|
+
|
|
53
|
+
benchmark-clean-charts:
|
|
54
|
+
rm -f *_chart_*.png
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import http from 'k6/http';
|
|
2
|
+
import { sleep } from 'k6';
|
|
3
|
+
import { Trend, Counter, Rate } from 'k6/metrics';
|
|
4
|
+
|
|
5
|
+
// Mao
|
|
6
|
+
|
|
7
|
+
const baseUrlToBaseUrlName = {
|
|
8
|
+
'https://ifh-core-api-dr-benchmark-048d364b548f5d9790082d8ba4fb44d8.us.langgraph.app': 'DRC,API S,OPS S,1 Job per',
|
|
9
|
+
'https://ifh-benchmarks-ba3f7d811fb6564ebe5248bcd7a0a662.us.langgraph.app': 'API S, OPS S, 1 Job per',
|
|
10
|
+
'https://wfh-benchmark-distributed-r-1603f73b1a175234b9b0fb1f9beea4f1.us.langgraph.app': 'DR,API S,OPS S,1 Job per',
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Metrics
|
|
14
|
+
const runDuration = new Trend('run_duration'); // ms for successful runs
|
|
15
|
+
const runPickUpDuration = new Trend('run_pickup_duration');
|
|
16
|
+
const runReturnDuration = new Trend('run_return_duration');
|
|
17
|
+
const runInsertionDuration = new Trend('run_insertion_duration');
|
|
18
|
+
const runOSSDuration = new Trend('run_oss_duration');
|
|
19
|
+
const successfulRuns = new Counter('successful_runs');
|
|
20
|
+
const failedRuns = new Counter('failed_runs');
|
|
21
|
+
const capacitySuccessRate = new Rate('capacity_success_rate');
|
|
22
|
+
|
|
23
|
+
// Env
|
|
24
|
+
const BASE_URL = __ENV.BASE_URL;
|
|
25
|
+
const LANGSMITH_API_KEY = __ENV.LANGSMITH_API_KEY;
|
|
26
|
+
|
|
27
|
+
const TARGET = parseInt(__ENV.TARGET || '10');
|
|
28
|
+
const WAIT_SECONDS = parseInt(__ENV.WAIT_SECONDS || '60');
|
|
29
|
+
const SUCCESS_THRESHOLD = parseFloat(__ENV.SUCCESS_THRESHOLD || '0.99');
|
|
30
|
+
|
|
31
|
+
// Agent params
|
|
32
|
+
const DATA_SIZE = parseInt(__ENV.DATA_SIZE || '1000');
|
|
33
|
+
const DELAY = parseInt(__ENV.DELAY || '0');
|
|
34
|
+
const EXPAND = parseInt(__ENV.EXPAND || '10');
|
|
35
|
+
const STEPS = parseInt(__ENV.STEPS || '10');
|
|
36
|
+
|
|
37
|
+
// Options
|
|
38
|
+
export const options = {
|
|
39
|
+
scenarios: {
|
|
40
|
+
capacity_single_shot: {
|
|
41
|
+
executor: 'shared-iterations',
|
|
42
|
+
vus: TARGET,
|
|
43
|
+
iterations: TARGET,
|
|
44
|
+
maxDuration: `${Math.max(WAIT_SECONDS + 120, 150)}s`,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
thresholds: {
|
|
48
|
+
'capacity_success_rate': [`rate>=${SUCCESS_THRESHOLD}`],
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function headers() {
|
|
53
|
+
const h = { 'Content-Type': 'application/json' };
|
|
54
|
+
if (LANGSMITH_API_KEY) h['x-api-key'] = LANGSMITH_API_KEY;
|
|
55
|
+
return h;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function buildPayload() {
|
|
59
|
+
return JSON.stringify({
|
|
60
|
+
assistant_id: 'benchmark',
|
|
61
|
+
input: {
|
|
62
|
+
data_size: DATA_SIZE,
|
|
63
|
+
delay: DELAY,
|
|
64
|
+
expand: EXPAND,
|
|
65
|
+
steps: STEPS,
|
|
66
|
+
},
|
|
67
|
+
config: {
|
|
68
|
+
recursion_limit: STEPS + 2,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export default function () {
|
|
74
|
+
// one-shot per iteration: create → wait → poll status once
|
|
75
|
+
const payload = buildPayload();
|
|
76
|
+
const reqHeaders = headers();
|
|
77
|
+
|
|
78
|
+
let threadId;
|
|
79
|
+
let runId;
|
|
80
|
+
let createdAt;
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const tRes = http.post(`${BASE_URL}/threads`, payload, {
|
|
84
|
+
headers: reqHeaders,
|
|
85
|
+
timeout: '60s',
|
|
86
|
+
});
|
|
87
|
+
if (tRes.status !== 200) {
|
|
88
|
+
failedRuns.add(1);
|
|
89
|
+
capacitySuccessRate.add(0);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const t = tRes.json();
|
|
93
|
+
threadId = t.thread_id;
|
|
94
|
+
|
|
95
|
+
createdAt = new Date().getTime();
|
|
96
|
+
const rRes = http.post(`${BASE_URL}/threads/${threadId}/runs`, payload, {
|
|
97
|
+
headers: reqHeaders,
|
|
98
|
+
timeout: '60s',
|
|
99
|
+
});
|
|
100
|
+
if (rRes.status !== 200) {
|
|
101
|
+
failedRuns.add(1);
|
|
102
|
+
capacitySuccessRate.add(0);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const r = rRes.json();
|
|
106
|
+
runId = r.run_id;
|
|
107
|
+
} catch (e) {
|
|
108
|
+
// Any network error counts as a failure — no retries
|
|
109
|
+
failedRuns.add(1);
|
|
110
|
+
capacitySuccessRate.add(0);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Sleep the configured wait time, exactly once
|
|
115
|
+
sleep(WAIT_SECONDS);
|
|
116
|
+
|
|
117
|
+
// Poll exactly once
|
|
118
|
+
try {
|
|
119
|
+
const gRes = http.get(`${BASE_URL}/threads/${threadId}/runs/${runId}`, {
|
|
120
|
+
headers: reqHeaders,
|
|
121
|
+
timeout: '30s',
|
|
122
|
+
});
|
|
123
|
+
const tRes = http.get(`${BASE_URL}/threads/${threadId}/state`, {
|
|
124
|
+
headers: reqHeaders,
|
|
125
|
+
timeout: '30s',
|
|
126
|
+
});
|
|
127
|
+
if (gRes.status !== 200) {
|
|
128
|
+
failedRuns.add(1);
|
|
129
|
+
capacitySuccessRate.add(0);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const run = gRes.json();
|
|
133
|
+
const t = tRes.json();
|
|
134
|
+
if (run.status === 'success') {
|
|
135
|
+
successfulRuns.add(1);
|
|
136
|
+
capacitySuccessRate.add(1);
|
|
137
|
+
try {
|
|
138
|
+
const insertionMs = new Date(run.created_at).getTime() - createdAt;
|
|
139
|
+
if (!Number.isNaN(insertionMs) && insertionMs >= 0) {
|
|
140
|
+
runInsertionDuration.add(insertionMs);
|
|
141
|
+
}
|
|
142
|
+
const durMs = new Date(run.updated_at).getTime() - createdAt;
|
|
143
|
+
if (!Number.isNaN(durMs) && durMs >= 0) {
|
|
144
|
+
runDuration.add(durMs);
|
|
145
|
+
}
|
|
146
|
+
const pickupDurMs = new Date(t.values.start_time).getTime() - new Date(run.created_at).getTime();
|
|
147
|
+
if (!Number.isNaN(pickupDurMs) && pickupDurMs >= 0) {
|
|
148
|
+
runPickUpDuration.add(pickupDurMs);
|
|
149
|
+
}
|
|
150
|
+
// Note: we are missing the time from `set_joint_status` to actually return to the client (ideally this is negligible)
|
|
151
|
+
const returnDurMs = new Date(run.updated_at).getTime() - new Date(t.values.end_time).getTime();
|
|
152
|
+
if (!Number.isNaN(returnDurMs) && returnDurMs >= 0) {
|
|
153
|
+
runReturnDuration.add(returnDurMs);
|
|
154
|
+
}
|
|
155
|
+
const ossDurMs = new Date(t.values.end_time).getTime() - new Date(t.values.start_time).getTime();
|
|
156
|
+
if (!Number.isNaN(ossDurMs) && ossDurMs >= 0) {
|
|
157
|
+
runOSSDuration.add(ossDurMs);
|
|
158
|
+
}
|
|
159
|
+
} catch (_) {
|
|
160
|
+
// ignore duration parsing errors
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
failedRuns.add(1);
|
|
164
|
+
capacitySuccessRate.add(0);
|
|
165
|
+
}
|
|
166
|
+
} catch (e) {
|
|
167
|
+
failedRuns.add(1);
|
|
168
|
+
capacitySuccessRate.add(0);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function handleSummary(data) {
|
|
173
|
+
const ts = new Date().toISOString().replace(/:/g, '-').replace(/\..+/, '');
|
|
174
|
+
|
|
175
|
+
const total = (data.metrics.successful_runs?.values?.count || 0) + (data.metrics.failed_runs?.values?.count || 0);
|
|
176
|
+
const succ = data.metrics.successful_runs?.values?.count || 0;
|
|
177
|
+
const fail = data.metrics.failed_runs?.values?.count || 0;
|
|
178
|
+
const successRate = total > 0 ? (succ / total) * 100 : 0;
|
|
179
|
+
|
|
180
|
+
function withStats(metric) {
|
|
181
|
+
if (!data.metrics[metric]?.values) return {};
|
|
182
|
+
const vals = data.metrics[metric].values;
|
|
183
|
+
return {
|
|
184
|
+
avg: vals.avg ? vals.avg / 1000 : null,
|
|
185
|
+
p50: vals.med ? vals.med / 1000 : null,
|
|
186
|
+
p95: vals['p(95)'] ? vals['p(95)'] / 1000 : null,
|
|
187
|
+
max: vals.max ? vals.max / 1000 : null,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const summary = {
|
|
192
|
+
timestamp: ts,
|
|
193
|
+
settings: {
|
|
194
|
+
baseUrl: BASE_URL,
|
|
195
|
+
baseUrlName: baseUrlToBaseUrlName[BASE_URL],
|
|
196
|
+
target: TARGET,
|
|
197
|
+
waitSeconds: WAIT_SECONDS,
|
|
198
|
+
dataSize: DATA_SIZE,
|
|
199
|
+
delay: DELAY,
|
|
200
|
+
expand: EXPAND,
|
|
201
|
+
steps: STEPS,
|
|
202
|
+
},
|
|
203
|
+
metrics: {
|
|
204
|
+
totalRuns: total,
|
|
205
|
+
successfulRuns: succ,
|
|
206
|
+
failedRuns: fail,
|
|
207
|
+
successRate,
|
|
208
|
+
runDuration: withStats('run_duration'),
|
|
209
|
+
runPickupDuration: withStats('run_pickup_duration'),
|
|
210
|
+
runReturnDuration: withStats('run_return_duration'),
|
|
211
|
+
runInsertionDuration: withStats('run_insertion_duration'),
|
|
212
|
+
runOSSDuration: withStats('run_oss_duration'),
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const fname = `capacity_summary_t${TARGET}_${ts}.json`;
|
|
217
|
+
return {
|
|
218
|
+
[fname]: JSON.stringify(summary, null, 2),
|
|
219
|
+
stdout: JSON.stringify(summary),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Adaptive capacity benchmark orchestrator.
|
|
3
|
+
* Ramps TARGET from RAMP_START up to RAMP_END by RAMP_MULTIPLIER.
|
|
4
|
+
* For each level N: optional cleanup → run k6 (N users, 1 run each) → wait summary → decide next.
|
|
5
|
+
* No retries anywhere; errors reduce success rate.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execFileSync } from 'node:child_process';
|
|
9
|
+
import { readdirSync, readFileSync, writeFileSync, createReadStream } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import readline from 'node:readline';
|
|
12
|
+
import QuickChart from 'quickchart-js';
|
|
13
|
+
|
|
14
|
+
function envBool(name, def = false) {
|
|
15
|
+
const v = process.env[name];
|
|
16
|
+
if (v === undefined || v === null) return def;
|
|
17
|
+
return String(v).toLowerCase() === 'true';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function envInt(name, def) {
|
|
21
|
+
const v = process.env[name];
|
|
22
|
+
if (!v) return def;
|
|
23
|
+
const n = parseInt(v, 10);
|
|
24
|
+
return Number.isFinite(n) ? n : def;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function envFloat(name, def) {
|
|
28
|
+
const v = process.env[name];
|
|
29
|
+
if (!v) return def;
|
|
30
|
+
const n = parseFloat(v);
|
|
31
|
+
return Number.isFinite(n) ? n : def;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const BASE_URL = process.env.BASE_URL;
|
|
35
|
+
const LANGSMITH_API_KEY = process.env.LANGSMITH_API_KEY;
|
|
36
|
+
|
|
37
|
+
const RAMP_START = envInt('RAMP_START', 10);
|
|
38
|
+
const RAMP_END = envInt('RAMP_END', 1000);
|
|
39
|
+
const RAMP_MULTIPLIER = envFloat('RAMP_MULTIPLIER', 10);
|
|
40
|
+
const WAIT_SECONDS = envInt('WAIT_SECONDS', 60);
|
|
41
|
+
const SUCCESS_THRESHOLD = envFloat('SUCCESS_THRESHOLD', 0.99);
|
|
42
|
+
const CLEAR_BETWEEN_STEPS = envBool('CLEAR_BETWEEN_STEPS', true);
|
|
43
|
+
const CLEAR_DELAY_SECONDS = envInt('CLEAR_DELAY_SECONDS', 5);
|
|
44
|
+
|
|
45
|
+
// Agent params
|
|
46
|
+
const DATA_SIZE = envInt('DATA_SIZE', 1000);
|
|
47
|
+
const DELAY = envInt('DELAY', 0);
|
|
48
|
+
const EXPAND = envInt('EXPAND', 50);
|
|
49
|
+
const STEPS = envInt('STEPS', 10);
|
|
50
|
+
|
|
51
|
+
if (!BASE_URL) {
|
|
52
|
+
console.error('BASE_URL is required');
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
if (!(RAMP_MULTIPLIER > 1)) {
|
|
56
|
+
console.error('RAMP_MULTIPLIER must be > 1');
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function headers() {
|
|
61
|
+
const h = { 'Content-Type': 'application/json' };
|
|
62
|
+
if (LANGSMITH_API_KEY) h['x-api-key'] = LANGSMITH_API_KEY;
|
|
63
|
+
return h;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function cleanThreads() {
|
|
67
|
+
if (!CLEAR_BETWEEN_STEPS) return;
|
|
68
|
+
const hdrs = headers();
|
|
69
|
+
const searchUrl = `${BASE_URL}/threads/search`;
|
|
70
|
+
let totalDeleted = 0;
|
|
71
|
+
// Loop until no more threads
|
|
72
|
+
while (true) {
|
|
73
|
+
const res = await fetch(searchUrl, {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: hdrs,
|
|
76
|
+
body: JSON.stringify({ limit: 1000 }),
|
|
77
|
+
});
|
|
78
|
+
if (!res.ok) {
|
|
79
|
+
console.error(`Cleanup search failed: ${res.status} ${res.statusText}`);
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
const threads = await res.json();
|
|
83
|
+
if (!Array.isArray(threads) || threads.length === 0) break;
|
|
84
|
+
for (const t of threads) {
|
|
85
|
+
try {
|
|
86
|
+
const del = await fetch(`${BASE_URL}/threads/${t.thread_id}`, {
|
|
87
|
+
method: 'DELETE',
|
|
88
|
+
headers: hdrs,
|
|
89
|
+
});
|
|
90
|
+
if (del.ok) totalDeleted++;
|
|
91
|
+
} catch (e) {
|
|
92
|
+
// Ignore delete errors; do not retry
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (CLEAR_DELAY_SECONDS > 0) {
|
|
97
|
+
await new Promise((r) => setTimeout(r, CLEAR_DELAY_SECONDS * 1000));
|
|
98
|
+
}
|
|
99
|
+
console.log(`Cleanup completed. Deleted ~${totalDeleted} threads.`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function runK6(target) {
|
|
103
|
+
const env = {
|
|
104
|
+
...process.env,
|
|
105
|
+
BASE_URL,
|
|
106
|
+
LANGSMITH_API_KEY,
|
|
107
|
+
TARGET: String(target),
|
|
108
|
+
WAIT_SECONDS: String(WAIT_SECONDS),
|
|
109
|
+
SUCCESS_THRESHOLD: String(SUCCESS_THRESHOLD),
|
|
110
|
+
DATA_SIZE: String(DATA_SIZE),
|
|
111
|
+
DELAY: String(DELAY),
|
|
112
|
+
EXPAND: String(EXPAND),
|
|
113
|
+
STEPS: String(STEPS),
|
|
114
|
+
};
|
|
115
|
+
console.log(`Running k6 with TARGET=${target}`);
|
|
116
|
+
// Also write raw JSON stream for per-stage histograms
|
|
117
|
+
const ts = new Date().toISOString().replace(/:/g, '-').replace(/\..+/, '');
|
|
118
|
+
const rawOut = `capacity_raw_t${target}_${ts}.json`;
|
|
119
|
+
// We rely on handleSummary to write capacity_summary_t${TARGET}_<ts>.json
|
|
120
|
+
execFileSync('k6', ['run', '--out', `json=${rawOut}`, 'capacity_k6.js'], {
|
|
121
|
+
cwd: process.cwd(),
|
|
122
|
+
env,
|
|
123
|
+
stdio: 'inherit',
|
|
124
|
+
});
|
|
125
|
+
return { rawOut, ts };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function loadSummaryForTarget(target) {
|
|
129
|
+
const files = readdirSync(process.cwd())
|
|
130
|
+
.filter((f) => f.startsWith(`capacity_summary_t${target}_`) && f.endsWith('.json'))
|
|
131
|
+
.sort();
|
|
132
|
+
if (files.length === 0) {
|
|
133
|
+
throw new Error(`No capacity summary file found for target ${target}`);
|
|
134
|
+
}
|
|
135
|
+
const latest = files[files.length - 1];
|
|
136
|
+
const content = readFileSync(join(process.cwd(), latest), 'utf-8');
|
|
137
|
+
return JSON.parse(content);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function main() {
|
|
141
|
+
let n = RAMP_START;
|
|
142
|
+
let lastSuccess = null; // { target, avgDurationSeconds, successRate }
|
|
143
|
+
let failedStep = null; // { target, successRate }
|
|
144
|
+
|
|
145
|
+
while (n <= RAMP_END) {
|
|
146
|
+
console.log(`\n=== Capacity step: N=${n} ===`);
|
|
147
|
+
if (CLEAR_BETWEEN_STEPS) {
|
|
148
|
+
console.log('Clearing threads before step...');
|
|
149
|
+
await cleanThreads();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const { rawOut, ts } = runK6(n);
|
|
154
|
+
try {
|
|
155
|
+
await generateHistogramsForStage(rawOut, n, ts);
|
|
156
|
+
} catch (e) {
|
|
157
|
+
console.error(`Failed to generate histograms for N=${n}:`, e?.message || e);
|
|
158
|
+
}
|
|
159
|
+
} catch (e) {
|
|
160
|
+
console.error(`k6 run failed at N=${n}:`, e?.message || e);
|
|
161
|
+
// Treat as failure for this step
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let successRate = 0;
|
|
165
|
+
let avgDurationSeconds = null;
|
|
166
|
+
try {
|
|
167
|
+
const summary = loadSummaryForTarget(n);
|
|
168
|
+
const s = summary?.metrics?.successRate; // percent
|
|
169
|
+
successRate = Number.isFinite(s) ? s / 100 : 0;
|
|
170
|
+
avgDurationSeconds = summary?.metrics?.averageDurationSeconds ?? null;
|
|
171
|
+
console.log(`Step N=${n} successRate=${(successRate * 100).toFixed(2)}% avgDur=${avgDurationSeconds ?? 'n/a'}s`);
|
|
172
|
+
} catch (e) {
|
|
173
|
+
console.error(`Failed to read summary for N=${n}:`, e?.message || e);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (successRate >= SUCCESS_THRESHOLD) {
|
|
177
|
+
lastSuccess = { target: n, avgDurationSeconds, successRate };
|
|
178
|
+
// next n
|
|
179
|
+
const next = Math.floor(n * RAMP_MULTIPLIER);
|
|
180
|
+
if (next <= n) {
|
|
181
|
+
console.log('Next ramp value would not increase; stopping.');
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
n = next;
|
|
185
|
+
} else {
|
|
186
|
+
failedStep = { target: n, successRate };
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const ts = new Date().toISOString().replace(/:/g, '-').replace(/\..+/, '');
|
|
192
|
+
const report = {
|
|
193
|
+
timestamp: ts,
|
|
194
|
+
ramp: { start: RAMP_START, end: RAMP_END, multiplier: RAMP_MULTIPLIER },
|
|
195
|
+
waitSeconds: WAIT_SECONDS,
|
|
196
|
+
threshold: SUCCESS_THRESHOLD,
|
|
197
|
+
last_success_target: lastSuccess?.target ?? 0,
|
|
198
|
+
last_success_avg_duration_seconds: lastSuccess?.avgDurationSeconds ?? null,
|
|
199
|
+
last_success_rate: lastSuccess?.successRate ?? null,
|
|
200
|
+
failed_target: failedStep?.target ?? null,
|
|
201
|
+
failed_success_rate: failedStep?.successRate ?? null,
|
|
202
|
+
};
|
|
203
|
+
const fname = `capacity_report_${ts}.json`;
|
|
204
|
+
writeFileSync(join(process.cwd(), fname), JSON.stringify(report, null, 2));
|
|
205
|
+
|
|
206
|
+
// Export selected fields as GitHub Action outputs if available
|
|
207
|
+
if (process.env.GITHUB_OUTPUT) {
|
|
208
|
+
const out = [
|
|
209
|
+
`last_success_target=${report.last_success_target}`,
|
|
210
|
+
`last_success_avg_duration_seconds=${report.last_success_avg_duration_seconds}`,
|
|
211
|
+
`failed_target=${report.failed_target}`,
|
|
212
|
+
`failed_success_rate=${report.failed_success_rate}`,
|
|
213
|
+
].join('\n');
|
|
214
|
+
writeFileSync(process.env.GITHUB_OUTPUT, `${out}\n`, { flag: 'a' });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
console.log('=== Capacity Benchmark Report ===');
|
|
218
|
+
console.log(`Last successful step: ${report.last_success_target}`);
|
|
219
|
+
console.log(`Average duration (s) at success: ${report.last_success_avg_duration_seconds}`);
|
|
220
|
+
console.log(`Failed step: ${report.failed_target} with success rate: ${report.failed_success_rate}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
main().catch((e) => {
|
|
224
|
+
console.error('Fatal error in capacity runner:', e?.stack || e?.message || e);
|
|
225
|
+
process.exit(1);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Build and save histogram charts for one stage from raw K6 JSON
|
|
229
|
+
async function generateHistogramsForStage(rawFile, target, ts) {
|
|
230
|
+
// Parse streaming JSONL from k6 --out json
|
|
231
|
+
const metrics = {
|
|
232
|
+
run_duration: [],
|
|
233
|
+
run_pickup_duration: [],
|
|
234
|
+
run_return_duration: [],
|
|
235
|
+
run_insertion_duration: [],
|
|
236
|
+
run_oss_duration: [],
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
await new Promise((resolve, reject) => {
|
|
240
|
+
const rl = readline.createInterface({ input: createReadStream(join(process.cwd(), rawFile), { encoding: 'utf-8' }), crlfDelay: Infinity });
|
|
241
|
+
rl.on('line', (line) => {
|
|
242
|
+
try {
|
|
243
|
+
const entry = JSON.parse(line);
|
|
244
|
+
if (entry.type === 'Point') {
|
|
245
|
+
const name = entry.metric;
|
|
246
|
+
if (name in metrics) {
|
|
247
|
+
const v = entry?.data?.value;
|
|
248
|
+
if (Number.isFinite(v)) metrics[name].push(v);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
} catch (_) {
|
|
252
|
+
// ignore parse errors
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
rl.on('close', resolve);
|
|
256
|
+
rl.on('error', reject);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// Build pie chart for component breakdown based on average seconds
|
|
260
|
+
const avg = (arr) => (arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : 0);
|
|
261
|
+
const avgInsertionS = avg(metrics.run_insertion_duration) / 1000;
|
|
262
|
+
const avgPickupS = avg(metrics.run_pickup_duration) / 1000;
|
|
263
|
+
const avgOssS = avg(metrics.run_oss_duration) / 1000;
|
|
264
|
+
const avgReturnS = avg(metrics.run_return_duration) / 1000;
|
|
265
|
+
const parts = [avgInsertionS, avgPickupS, avgOssS, avgReturnS];
|
|
266
|
+
const totalParts = parts.reduce((a, b) => a + b, 0);
|
|
267
|
+
if (totalParts > 0) {
|
|
268
|
+
const chart = new QuickChart();
|
|
269
|
+
chart.setWidth(700);
|
|
270
|
+
chart.setHeight(500);
|
|
271
|
+
chart.setFormat('png');
|
|
272
|
+
chart.setConfig({
|
|
273
|
+
type: 'pie',
|
|
274
|
+
data: {
|
|
275
|
+
labels: ['Insertion', 'Pickup', 'OSS', 'Return'],
|
|
276
|
+
datasets: [{
|
|
277
|
+
label: `Breakdown of Run Duration (N=${target})`,
|
|
278
|
+
data: parts.map((v) => Number(v.toFixed(4))),
|
|
279
|
+
backgroundColor: [
|
|
280
|
+
'rgba(255, 99, 132, 0.6)',
|
|
281
|
+
'rgba(54, 162, 235, 0.6)',
|
|
282
|
+
'rgba(255, 206, 86, 0.6)',
|
|
283
|
+
'rgba(75, 192, 192, 0.6)',
|
|
284
|
+
],
|
|
285
|
+
borderColor: [
|
|
286
|
+
'rgba(255, 99, 132, 1)',
|
|
287
|
+
'rgba(54, 162, 235, 1)',
|
|
288
|
+
'rgba(255, 206, 86, 1)',
|
|
289
|
+
'rgba(75, 192, 192, 1)',
|
|
290
|
+
],
|
|
291
|
+
borderWidth: 1,
|
|
292
|
+
}],
|
|
293
|
+
},
|
|
294
|
+
options: {
|
|
295
|
+
plugins: {
|
|
296
|
+
title: { display: true, text: `Run Duration Breakdown (s) — N=${target}` },
|
|
297
|
+
legend: { position: 'right' },
|
|
298
|
+
tooltip: { callbacks: { label: (ctx) => `${ctx.label}: ${ctx.parsed.toFixed(3)}s` } },
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
const pieBuf = await chart.toBinary();
|
|
303
|
+
const piePath = join(process.cwd(), `capacity_pie_breakdown_t${target}_${ts}.png`);
|
|
304
|
+
writeFileSync(piePath, pieBuf);
|
|
305
|
+
console.log(`Saved pie chart: ${piePath}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Parse benchmark JSON results and send to Datadog.
|
|
4
|
+
Install: pip install datadog-api-client
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import glob
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
from datadog_api_client import ApiClient, Configuration
|
|
14
|
+
from datadog_api_client.v2.api.logs_api import LogsApi
|
|
15
|
+
from datadog_api_client.v2.model.http_log import HTTPLog
|
|
16
|
+
from datadog_api_client.v2.model.http_log_item import HTTPLogItem
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def send_benchmark_results(
|
|
20
|
+
benchmark_data, common_labels=None, dd_site="us5.datadoghq.com", api_key=None
|
|
21
|
+
):
|
|
22
|
+
"""
|
|
23
|
+
Send benchmark JSON to Datadog.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
benchmark_data: Dict containing 'settings' and 'metrics' from benchmark
|
|
27
|
+
common_labels: Additional labels (e.g., base_url)
|
|
28
|
+
"""
|
|
29
|
+
configuration = Configuration()
|
|
30
|
+
configuration.server_variables["site"] = dd_site
|
|
31
|
+
configuration.api_key["apiKeyAuth"] = api_key
|
|
32
|
+
|
|
33
|
+
if common_labels:
|
|
34
|
+
benchmark_data["labels"] = common_labels
|
|
35
|
+
|
|
36
|
+
log_item = HTTPLogItem(
|
|
37
|
+
ddsource="benchmark",
|
|
38
|
+
ddtags=f"env:benchmarking,base_url:{common_labels.get('base_url', 'unknown')}"
|
|
39
|
+
if common_labels
|
|
40
|
+
else "env:benchmarking",
|
|
41
|
+
hostname=os.getenv("HOSTNAME", "localhost"),
|
|
42
|
+
message=json.dumps(benchmark_data),
|
|
43
|
+
service="benchmark-results",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
with ApiClient(configuration) as api_client:
|
|
47
|
+
api_instance = LogsApi(api_client)
|
|
48
|
+
body = HTTPLog([log_item])
|
|
49
|
+
|
|
50
|
+
api_instance.submit_log(body=body)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def process_benchmark_file(
|
|
54
|
+
json_file, common_labels=None, dd_site="us5.datadoghq.com", api_key=None
|
|
55
|
+
):
|
|
56
|
+
"""
|
|
57
|
+
Read benchmark JSON file and send to Datadog.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
json_file: Path to benchmark results JSON file
|
|
61
|
+
common_labels: Additional labels for all metrics
|
|
62
|
+
"""
|
|
63
|
+
with open(json_file) as f:
|
|
64
|
+
data = json.load(f)
|
|
65
|
+
|
|
66
|
+
send_benchmark_results(data, common_labels, dd_site, api_key)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
parser = argparse.ArgumentParser(description="Send benchmark results to Datadog")
|
|
71
|
+
parser.add_argument(
|
|
72
|
+
"benchmark_file", type=str, help="Path to benchmark results file"
|
|
73
|
+
)
|
|
74
|
+
args = parser.parse_args()
|
|
75
|
+
|
|
76
|
+
DD_API_KEY = os.getenv("DD_API_KEY")
|
|
77
|
+
if not DD_API_KEY:
|
|
78
|
+
sys.exit(1)
|
|
79
|
+
|
|
80
|
+
DD_SITE = os.getenv("DD_SITE", "us5.datadoghq.com")
|
|
81
|
+
|
|
82
|
+
common_labels = {"base_url": os.getenv("BASE_URL")}
|
|
83
|
+
|
|
84
|
+
for file in glob.glob(args.benchmark_file):
|
|
85
|
+
process_benchmark_file(file, common_labels, DD_SITE, DD_API_KEY)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.38"
|
|
@@ -24,7 +24,8 @@ async def meta_info(request: ApiRequest):
|
|
|
24
24
|
"flags": {
|
|
25
25
|
"assistants": True,
|
|
26
26
|
"crons": plus and config.FF_CRONS_ENABLED,
|
|
27
|
-
"langsmith": bool(config.
|
|
27
|
+
"langsmith": bool(config.LANGSMITH_CONTROL_PLANE_API_KEY)
|
|
28
|
+
and bool(config.TRACING),
|
|
28
29
|
"langsmith_tracing_replicas": True,
|
|
29
30
|
},
|
|
30
31
|
"host": {
|