-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat(benchmark): pool benchmark harness with standalone mock server and per-API QPS tracking #1518
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
Open
Pangjiping
wants to merge
16
commits into
opensandbox-group:main
Choose a base branch
from
Pangjiping:feat/pool-benchmark-harness
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 11 commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
264dbf5
feat(benchmark): add pool benchmark with standalone mock server and p…
Pangjiping 1e6c2d3
refactor(benchmark): build Kotlin SDK from source via Gradle composit…
Pangjiping af22946
feat(benchmark): support production pool profile knobs (acquireMinRem…
Pangjiping 51466ec
chore(benchmark): execd ping latency to fixed 100ms in the default pr…
Pangjiping d5e6bc6
feat(benchmark): add client-side instrumentation and explicit success…
Pangjiping 3e19bf9
feat(benchmark): consolidate all metrics into one run directory
Pangjiping 0f107a9
docs(benchmark): fix stale default-profile comment in quick start
Pangjiping 51419a1
fix(benchmark): steady-state await must cover the full configured dur…
Pangjiping 6f26b16
feat(pool): add warmup pipeline diagnostics to isolate warmupConcurre…
Pangjiping 5734b0d
feat(benchmark): shared connection pool size sweep (--shared-connecti…
Pangjiping e533355
fix(benchmark): serialize warmup pipeline PhaseStats as JSON objects,…
Pangjiping cc3903d
docs(benchmark): document warmup connection-churn problem and shared …
Pangjiping fae4b74
docs: document connection reuse at high warmup concurrency in the cli…
Pangjiping 8ba6040
feat(benchmark): enable the shared connection pool by default (auto-s…
Pangjiping 9c3ecc7
feat(benchmark): fixed 500-idle-slot shared pool by default
Pangjiping f367f7b
feat(benchmark): steady-start-immediately flag for startup-under-load…
Pangjiping File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
191 changes: 191 additions & 0 deletions
191
...lin/sandbox/src/main/kotlin/com/alibaba/opensandbox/sandbox/pool/PoolWarmupDiagnostics.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,191 @@ | ||
| /* | ||
| * Copyright 2026 Alibaba Group Holding Ltd. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package com.alibaba.opensandbox.sandbox.pool | ||
|
|
||
| /** | ||
| * Diagnostic counters for the warmup pipeline. | ||
| * | ||
| * Benchmark/diagnostic aid only — not part of the public API contract. | ||
| * Overhead is a single `System.nanoTime()` read plus a locked list append per | ||
| * warmup event, and it is a no-op when never read. State is global (all | ||
| * pools in the JVM share it), so reset before a single-pool experiment and | ||
| * snapshot after it settles. | ||
| * | ||
| * Measured phases of one warmup task: | ||
| * - queue wait: submission -> task picked up by a warmup worker | ||
| * - create: `createOneSandbox` (create + readiness + renew) | ||
| * - commit: lock + store putIdle when the sandbox enters the idle buffer | ||
| * Plus the reconcile-tick cadence (submission driver) and the in-flight | ||
| * warmup count trajectory. | ||
| */ | ||
| object PoolWarmupDiagnostics { | ||
| data class PhaseStats( | ||
| val count: Long, | ||
| val meanMs: Double, | ||
| val p50Ms: Long, | ||
| val p95Ms: Long, | ||
| val maxMs: Long, | ||
| ) | ||
|
|
||
| data class Snapshot( | ||
| val queueWaitMs: PhaseStats, | ||
| val createDurationMs: PhaseStats, | ||
| val commitDurationMs: PhaseStats, | ||
| val tickIntervalMs: PhaseStats, | ||
| val tickDurationMs: PhaseStats, | ||
| val submitBurst: PhaseStats, | ||
| val submitCalls: Long, | ||
| val inFlightPeak: Int, | ||
| val inFlightMean: Double, | ||
| val createFailures: Map<String, Long>, | ||
| ) | ||
|
|
||
| private val lock = Any() | ||
| private val queueWaitNanos = ArrayList<Long>() | ||
| private val createNanos = ArrayList<Long>() | ||
| private val commitNanos = ArrayList<Long>() | ||
| private val tickIntervalNanos = ArrayList<Long>() | ||
| private val tickDurationNanos = ArrayList<Long>() | ||
| private val submitBursts = ArrayList<Int>() | ||
| private val failureReasons = LinkedHashMap<String, Long>() | ||
| private var submitCalls = 0L | ||
| private var inFlightSum = 0L | ||
| private var inFlightSamples = 0L | ||
| private var inFlightPeak = 0 | ||
| private var lastTickNanos = 0L | ||
|
|
||
| fun reset() { | ||
| synchronized(lock) { | ||
| queueWaitNanos.clear() | ||
| createNanos.clear() | ||
| commitNanos.clear() | ||
| tickIntervalNanos.clear() | ||
| tickDurationNanos.clear() | ||
| submitBursts.clear() | ||
| failureReasons.clear() | ||
| submitCalls = 0L | ||
| inFlightSum = 0L | ||
| inFlightSamples = 0L | ||
| inFlightPeak = 0 | ||
| lastTickNanos = 0L | ||
| } | ||
| } | ||
|
|
||
| fun recordQueueWait(nanos: Long) = synchronized(lock) { queueWaitNanos.add(nanos) } | ||
|
|
||
| fun recordCreate(nanos: Long) = synchronized(lock) { createNanos.add(nanos) } | ||
|
|
||
| fun recordCommit(nanos: Long) = synchronized(lock) { commitNanos.add(nanos) } | ||
|
|
||
| /** Records the wall-clock spacing between reconcile ticks plus each tick's execution time. */ | ||
| fun recordTick(nowNanos: Long, durationNanos: Long) { | ||
| synchronized(lock) { | ||
| tickDurationNanos.add(durationNanos) | ||
| if (lastTickNanos != 0L) { | ||
| tickIntervalNanos.add(nowNanos - lastTickNanos) | ||
| } | ||
| lastTickNanos = nowNanos | ||
| } | ||
| } | ||
|
|
||
| fun recordSubmitBurst(size: Int) { | ||
| synchronized(lock) { | ||
| submitCalls++ | ||
| submitBursts.add(size) | ||
| } | ||
| } | ||
|
|
||
| fun recordInFlight(current: Int) { | ||
| synchronized(lock) { | ||
| if (current > inFlightPeak) inFlightPeak = current | ||
| inFlightSum += current | ||
| inFlightSamples++ | ||
| } | ||
| } | ||
|
|
||
| /** Records a failed createOneSandbox attempt: exception class + first words of the message. */ | ||
| fun recordCreateFailure(failure: Throwable) { | ||
| val message = failure.message?.trim()?.take(80) ?: "" | ||
| val key = "${failure.javaClass.simpleName}: $message" | ||
| synchronized(lock) { | ||
| failureReasons[key] = (failureReasons[key] ?: 0L) + 1 | ||
| } | ||
| } | ||
|
|
||
| fun snapshot(): Snapshot { | ||
| val queueWait: LongArray | ||
| val create: LongArray | ||
| val commit: LongArray | ||
| val tickInterval: LongArray | ||
| val tickDuration: LongArray | ||
| val bursts: IntArray | ||
| val calls: Long | ||
| val peak: Int | ||
| val meanInFlight: Double | ||
| val failures: Map<String, Long> | ||
| synchronized(lock) { | ||
| queueWait = queueWaitNanos.toLongArray() | ||
| create = createNanos.toLongArray() | ||
| commit = commitNanos.toLongArray() | ||
| tickInterval = tickIntervalNanos.toLongArray() | ||
| tickDuration = tickDurationNanos.toLongArray() | ||
| bursts = submitBursts.toIntArray() | ||
| calls = submitCalls | ||
| peak = inFlightPeak | ||
| meanInFlight = if (inFlightSamples == 0L) 0.0 else inFlightSum.toDouble() / inFlightSamples | ||
| failures = failureReasons.toMap() | ||
| } | ||
| return Snapshot( | ||
| queueWaitMs = phase(queueWait), | ||
| createDurationMs = phase(create), | ||
| commitDurationMs = phase(commit), | ||
| tickIntervalMs = phase(tickInterval), | ||
| tickDurationMs = phase(tickDuration), | ||
| submitBurst = burstPhase(bursts), | ||
| submitCalls = calls, | ||
| inFlightPeak = peak, | ||
| inFlightMean = meanInFlight, | ||
| createFailures = failures, | ||
| ) | ||
| } | ||
|
|
||
| private fun phase(nanos: LongArray): PhaseStats { | ||
| if (nanos.isEmpty()) return PhaseStats(0, 0.0, 0L, 0L, 0L) | ||
| val sorted = nanos.copyOf() | ||
| sorted.sort() | ||
| return PhaseStats( | ||
| count = sorted.size.toLong(), | ||
| meanMs = sorted.average() / 1_000_000.0, | ||
| p50Ms = sorted[(sorted.size - 1) / 2] / 1_000_000, | ||
| p95Ms = sorted[((sorted.size - 1) * 95) / 100] / 1_000_000, | ||
| maxMs = sorted.last() / 1_000_000, | ||
| ) | ||
| } | ||
|
|
||
| private fun burstPhase(bursts: IntArray): PhaseStats { | ||
| if (bursts.isEmpty()) return PhaseStats(0, 0.0, 0L, 0L, 0L) | ||
| val sorted = bursts.copyOf() | ||
| sorted.sort() | ||
| return PhaseStats( | ||
| count = sorted.size.toLong(), | ||
| meanMs = sorted.average(), | ||
| p50Ms = sorted[(sorted.size - 1) / 2].toLong(), | ||
| p95Ms = sorted[((sorted.size - 1) * 95) / 100].toLong(), | ||
| maxMs = sorted.last().toLong(), | ||
| ) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| bin/ | ||
| results/ |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When any production JVM uses
SandboxPool, the new unconditionalPoolWarmupDiagnostics.record*calls append every reconcile/warmup event into these globalArrayLists and the failure map, with no cap and no automatic reset. Because the object lives in the published SDK and is shared across all pools, a long-lived service that continuously replenishes sandboxes will retain one or more samples per warmup forever even if nobody reads diagnostics, so this is a memory leak plus a global lock hot path rather than the documented no-op.Useful? React with 👍 / 👎.