Skip to content
Merged
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
34 changes: 31 additions & 3 deletions benchmarks/CONNECTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,14 +310,39 @@ deadlines or the existing one-second sampling cadence.
The existing worker marker samples active, unfinished traffic sessions that have
received DATA and have had no composed read or write completion for at least one
second. This is an inactivity trigger, not evidence of a network failure. The
single IO worker queries at most 16 identities per tick, keeping eight samples
per identity in fixed storage below 32 KiB per process, in addition to the existing
single IO worker queries at most 16 identities per tick, keeping the first four
qualifying samples and latest four samples per retained identity in fixed storage below 32 KiB per process, in addition to the existing
16 MiB operation-trace budget (allocator overhead excluded). At each tick, observed
progress, completion or close releases the retained slot before admitting new
identities; persistent stalls retain their slots. Finite capacity can omit a
connection. Overwritten samples, released histories, slot reuse and omitted eligible
observations are counted. No new timer, thread or worker post is added.

Histories with up to eight observations keep every sample. On overflow, the
first four remain fixed and only the four-sample tail rotates; output stays in
observation order. `sample_ordinal` is one-based within that retained history,
`total_samples` counts its queries, and `overwritten_samples` counts the omitted
middle observations (`total_samples - 8` after overflow). Progress pruning or
slot reuse starts a new history and ordinal sequence; summary overwrite/query
counters remain cumulative, including histories later released.

`tcp_kernel_sample_history` now uses `schema: 2`, with
`retention_policy: "first_4_latest_4"` and `head_sample_capacity: 4` /
`tail_sample_capacity: 4`. The nested `tcp_kernel_samples` summary independently
declares schema 2 and the same retention metadata; its enclosing
`operation_trace_summary` stays schema 1. Older schema 1 kernel histories used a
tail-only ring and remain that historical format; never relabel or interpret
those raw files as onset-preserving evidence.

Kernel histories alone also carry `clock_domain: "local_steady_clock"`,
`clock_origin_us` and `clock_origin_unit: "microseconds_since_steady_clock_epoch"`.
The origin is the existing application `began` value converted to microseconds;
serialization performs no new clock or socket query. Add `sample_time_us` to
that origin to compare peer windows only when provenance establishes the same
host and steady-clock domain. These quantized values are not wall-clock time,
a cross-host clock mapping, or proof that peer observations were simultaneous.
Older records without an origin do not gain one retroactively.

On Apple platforms, samples query public `TCP_CONNECTION_INFO`: TCP state, flags,
RTO in milliseconds, queued send bytes (including in-flight data), send/receive
windows, congestion window, slow-start threshold and byte/retransmission counters.
Expand All @@ -333,7 +358,10 @@ inside the existing bounded trace output. `operation_trace_summary` contains
The final result adds `tcp_kernel_samples_enabled: true` only when enabled.
Successful runs emit no sample history. `sample_time_us` is the periodic query
start time in process-relative microseconds; it is distinct from operation-ring
freeze time. Sampling can miss transient states or be delayed by the IO worker.
freeze time. Sampling can miss transient states or be delayed by the IO worker. The retained
head begins with the earliest admitted qualifying query, after the one-second
inactivity threshold; it is not the onset of TCP recovery. Finite identity
admission and omitted middle samples can still leave gaps between peer windows.
A retained state after reset cannot describe earlier recovery; use actual sampled
state and time. Samples neither prove a root cause nor provide performance or
capacity acceptance. Keep the raw diagnostic output local.
Expand Down
24 changes: 22 additions & 2 deletions benchmarks/CONNECTIONS_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,11 +270,29 @@ macOS 的 `tcp_info` 还包含 TCP 状态、窗口、发送缓冲字节数、收

现有工作线程标记检查活动且未完成的 traffic 会话:会话已经收到 DATA,且至少一秒没有
组合读写完成。此条件只表示应用操作无进展,不能证明网络故障。单个 IO 工作线程每次最多
查询 16 个身份,每个身份保留 8 个样本;固定存储小于每进程 32 KiB,独立于原有 16 MiB
查询 16 个身份,每个身份保留最早 4 个合格样本与最新 4 个样本;固定存储小于每进程 32 KiB,独立于原有 16 MiB
操作历史预算,不含分配器额外开销。每次先释放已观察到进展、完成或关闭的会话槽位,
再接纳新身份;持续停滞的身份保留槽位。容量有限,不能保证每个连接都被采样。覆盖样本、
释放历史、槽位复用和未保留的合格观测次数均有计数,不增加计时器、线程或工作线程投递。

历史不超过 8 次观测时保留全部样本;超出后固定保留最早 4 次,只轮换尾部 4 次,
输出仍按观测顺序排列。`sample_ordinal` 从该历史的 1 开始,`total_samples` 统计其查询次数,
`overwritten_samples` 统计省略的中段样本(超过容量后为 `total_samples - 8`)。
进展清理或槽位复用会开始新的历史和序号;摘要的覆盖与查询计数仍累计包含已释放的历史。

`tcp_kernel_sample_history` 使用 `schema: 2`,并输出
`retention_policy: "first_4_latest_4"`、`head_sample_capacity: 4`、`tail_sample_capacity: 4`。
嵌套的 `tcp_kernel_samples` 摘要单独声明 schema 2 及相同保留元数据,
外层 `operation_trace_summary` 仍为 schema 1。旧 schema 1 内核历史使用仅保留尾部的环,
原始文件须保留原格式,不能重新标记或当作保留起始样本的证据。

仅内核历史还包含 `clock_domain: "local_steady_clock"`、`clock_origin_us`,以及
`clock_origin_unit: "microseconds_since_steady_clock_epoch"`。起点由现有应用 `began`
转换为微秒,输出时不新增时钟或 socket 查询。只有运行凭据确认两端位于同一主机、
使用同一 steady-clock 域时,才能用起点加 `sample_time_us` 比较两端窗口。
这些量化值不是墙上时间,不建立跨主机时钟映射,也不能证明两端观测同时发生;
没有起点的旧记录不能补上这一字段后重新解释。

Apple 平台通过公开 `TCP_CONNECTION_INFO` 查询 TCP 状态、flags、毫秒单位 RTO、发送队列
字节数(含在途数据)、收发窗口、拥塞窗口、慢启动阈值及字节/重传计数。其他平台输出
`tcp_info: null` 和 `unsupported_platform`;socket 已关闭、查询失败或返回结构过短时,
Expand All @@ -284,7 +302,9 @@ Apple 平台通过公开 `TCP_CONNECTION_INFO` 查询 TCP 状态、flags、毫
槽位及操作记录是否成功输出。`operation_trace_summary` 包含 `tcp_kernel_samples` 预算/计数、
`tcp_kernel_histories_emitted` 和 `tcp_kernel_histories_omitted`,输出截断仍有明确标记。仅启用时,最终结果增加 `tcp_kernel_samples_enabled: true`。
成功运行不输出采样历史。`sample_time_us` 是周期查询开始时的进程相对微秒数,区别于操作
历史冻结时间。采样可能错过瞬时状态,也可能因 IO 工作线程阻塞而延迟。reset 后的状态
历史冻结时间。采样可能错过瞬时状态,也可能因 IO 工作线程阻塞而延迟。
起始部分从超过一秒无进展阈值后、最早获准查询的合格观测开始,不是 TCP 进入恢复状态的时刻;
身份准入容量与被省略的中段样本仍可能造成两端窗口之间的缺口。reset 后的状态
不能描述此前的恢复过程,须按实际样本时间和状态解读。采样不能证明根因、性能或容量,
原始诊断输出仅留本地。

Expand Down
40 changes: 38 additions & 2 deletions benchmarks/asio_connections.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -930,10 +930,11 @@ void application::trace_failure() {
{
++kernel_histories_available;
kernel_histories_emitted += output.append([&](std::ostream& out) {
out << "{\"event\":\"tcp_kernel_sample_history\",\"schema\":1,\"role\":" << std::quoted(o.role)
out << "{\"event\":\"tcp_kernel_sample_history\",\"schema\":"
<< connection_load::tcp_kernel_samples::schema_version << ",\"role\":" << std::quoted(o.role)
<< ",\"run_id\":" << o.run << ",\"client_id\":" << value->state.owner.client
<< ",\"connection_id\":" << value->state.owner.connection << ',';
kernel_samples.write_history(out, value->state.owner);
kernel_samples.write_history(out, value->state.owner, began);
out << '}';
});
}
Expand Down Expand Up @@ -1525,6 +1526,41 @@ void self_test() {
require(kernel_enabled.deadline == enabled.deadline && kernel_enabled.request_ms == enabled.request_ms &&
kernel_enabled.connections == enabled.connections && kernel_enabled.window == enabled.window &&
kernel_enabled.payload == enabled.payload);
// Synthetic queries only: exercise retention/JSON without sockets or load.
connection_load::tcp_kernel_samples retained(true);
unsigned kernel_queries{};
for (unsigned index = 1; index <= 10; ++index)
{
retained.sample({7, 1, 1}, 1000000 + index, 1000000, 4, 2, 3, [&] {
++kernel_queries;
return connection_load::tcp_kernel_info{};
});
}
std::ostringstream kernel_history, kernel_summary;
kernel_history << "{\"schema\":" << connection_load::tcp_kernel_samples::schema_version << ',';
retained.write_history(kernel_history, {7, 1, 1}, clock_type::time_point{std::chrono::microseconds{1234567}});
kernel_history << '}';
retained.write_summary(kernel_summary);
require(kernel_queries == 10 &&
kernel_history.str().starts_with("{\"schema\":2,\"clock_domain\":\"local_steady_clock\","));
require(kernel_history.str().find(
"\"clock_origin_us\":1234567,\"clock_origin_unit\":\"microseconds_since_steady_clock_epoch\"") !=
std::string::npos);
require(kernel_history.str().find("\"head_sample_capacity\":4,\"tail_sample_capacity\":4") != std::string::npos);
require(kernel_history.str().find("\"total_samples\":10,\"overwritten_samples\":2}") != std::string::npos);
std::size_t kernel_position{};
for (const auto ordinal : {1, 2, 3, 4, 7, 8, 9, 10})
{
kernel_position = kernel_history.str().find("\"sample_ordinal\":" + std::to_string(ordinal) +
",\"sample_time_us\":" + std::to_string(1000000 + ordinal),
kernel_position);
require(kernel_position != std::string::npos);
++kernel_position;
}
require(kernel_history.str().find("\"sample_ordinal\":5,") == std::string::npos &&
kernel_history.str().find("\"sample_ordinal\":6,") == std::string::npos);
require(kernel_summary.str().starts_with("{\"schema\":2,\"retention_policy\":\"first_4_latest_4\","));
std::cout << "native control kernel retention schema 2 checks passed\n";
bool kernel_rejected{};
try
{ parse("off", false, true); }
Expand Down
53 changes: 37 additions & 16 deletions benchmarks/connections_kernel_samples.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <algorithm>
#include <array>
#include <cerrno>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <memory>
Expand Down Expand Up @@ -80,6 +81,8 @@ class tcp_kernel_samples
{
public:
static constexpr std::size_t session_capacity = 16, sample_capacity = 8, memory_limit = 32 * 1024;
static constexpr std::size_t head_sample_capacity = 4, tail_sample_capacity = 4;
static constexpr unsigned schema_version = 2;
static constexpr std::int64_t inactivity_us = 1000000;

explicit tcp_kernel_samples(bool enabled)
Expand Down Expand Up @@ -140,7 +143,7 @@ class tcp_kernel_samples
return; // Persistent stalls keep their slots; never query omitted identities.
}
value->completions = completions;
auto& event = value->events[value->total % sample_capacity];
auto& event = value->events[event_index(value->total)];
event = {time_us, idle_us, next_receive, next_send, query()};
overwritten_ += value->total >= sample_capacity;
++value->total;
Expand All @@ -159,24 +162,32 @@ class tcp_kernel_samples
return data_ && find(owner);
}

void write_history(std::ostream& out, identity owner) const {
void write_history(std::ostream& out, identity owner, std::chrono::steady_clock::time_point began) const {
const auto* value = data_ ? find(owner) : nullptr;
out << "\"sample_phase\":\"periodic_io_worker\",\"time_unit\":\"microseconds_since_process_start\",\"samples\":"
"[";
out << "\"clock_domain\":\"local_steady_clock\",\"clock_origin_us\":"
<< std::chrono::duration_cast<std::chrono::microseconds>(began.time_since_epoch()).count()
<< ",\"clock_origin_unit\":\"microseconds_since_steady_clock_epoch\","
"\"retention_policy\":\"first_4_latest_4\",\"head_sample_capacity\":"
<< head_sample_capacity << ",\"tail_sample_capacity\":" << tail_sample_capacity
<< ",\"sample_phase\":\"periodic_io_worker\",\"time_unit\":\"microseconds_since_process_start\","
"\"samples\":[";
if (value)
{
const auto size = (std::min)(value->total, std::uint64_t{sample_capacity});
const auto head_size = (std::min)(size, std::uint64_t{head_sample_capacity});
const auto tail_size = size - head_size;
for (std::uint64_t index = 0; index < size; ++index)
{
if (index)
{
out << ',';
}
const auto& event = value->events[(value->total - size + index) % sample_capacity];
const auto ordinal = index < head_size ? index : value->total - tail_size + index - head_size;
const auto& event = value->events[event_index(ordinal)];
const auto& info = event.info;
out << "{\"sample_time_us\":" << event.time_us << ",\"no_completion_age_us\":" << event.idle_us
<< ",\"next_receive\":" << event.next_receive << ",\"next_send\":" << event.next_send
<< ",\"tcp_info\":";
out << "{\"sample_ordinal\":" << ordinal + 1 << ",\"sample_time_us\":" << event.time_us
<< ",\"no_completion_age_us\":" << event.idle_us << ",\"next_receive\":" << event.next_receive
<< ",\"next_send\":" << event.next_send << ",\"tcp_info\":";
if (info.result == tcp_kernel_info::status::available)
{
out << "{\"state\":" << info.state << ",\"flags\":" << info.flags << ",\"rto_ms\":" << info.rto_ms
Expand Down Expand Up @@ -204,11 +215,11 @@ class tcp_kernel_samples
}
out << '}';
}
out << "],\"overwritten_samples\":" << value->total - size;
out << "],\"total_samples\":" << value->total << ",\"overwritten_samples\":" << value->total - size;
}
else
{
out << "],\"overwritten_samples\":0";
out << "],\"total_samples\":0,\"overwritten_samples\":0";
}
}

Expand All @@ -221,15 +232,25 @@ class tcp_kernel_samples
retained += value.used;
}
}
out << "{\"storage_bytes\":" << storage_bytes() << ",\"memory_limit_bytes\":" << memory_limit
<< ",\"session_capacity\":" << session_capacity << ",\"sample_capacity\":" << sample_capacity
<< ",\"retained_identities\":" << retained << ",\"released_histories\":" << released_
<< ",\"slot_reuses\":" << reused_ << ",\"omitted_eligible_observations\":" << omitted_
<< ",\"overwritten_samples\":" << overwritten_ << ",\"sampler_calls\":" << queries_
<< ",\"frozen\":" << (frozen_ ? "true" : "false") << '}';
out << "{\"schema\":" << schema_version
<< ",\"retention_policy\":\"first_4_latest_4\",\"head_sample_capacity\":" << head_sample_capacity
<< ",\"tail_sample_capacity\":" << tail_sample_capacity << ",\"storage_bytes\":" << storage_bytes()
<< ",\"memory_limit_bytes\":" << memory_limit << ",\"session_capacity\":" << session_capacity
<< ",\"sample_capacity\":" << sample_capacity << ",\"retained_identities\":" << retained
<< ",\"released_histories\":" << released_ << ",\"slot_reuses\":" << reused_
<< ",\"omitted_eligible_observations\":" << omitted_ << ",\"overwritten_samples\":" << overwritten_
<< ",\"sampler_calls\":" << queries_ << ",\"frozen\":" << (frozen_ ? "true" : "false") << '}';
}

private:
static_assert(head_sample_capacity + tail_sample_capacity == sample_capacity);

static std::size_t event_index(std::uint64_t ordinal) noexcept {
return ordinal < head_sample_capacity
? ordinal
: head_sample_capacity + (ordinal - head_sample_capacity) % tail_sample_capacity;
}

struct event
{
std::int64_t time_us{}, idle_us{};
Expand Down
Loading
Loading