@feltdb/core 0.12.0 → 0.12.1
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.
- package/dist/agent-state.d.ts +489 -0
- package/dist/agent-state.d.ts.map +1 -0
- package/dist/agent-state.js +993 -0
- package/dist/build-identity.json +19 -0
- package/dist/create/build-identity.json +19 -0
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/crates/feltdb/src/lib.rs +11 -0
- package/dist/create/server-source/crates/feltdb/src/query_performance.rs +4 -1
- package/dist/create/server-source/crates/feltdb/src/state_model.rs +854 -155
- package/dist/create/server-source/crates/feltdb/tests/generalized_version_control_substrate_completion.rs +31 -13
- package/dist/create/server-source/crates/feltdb/tests/state_ref_authority.rs +918 -0
- package/dist/create/server-source/crates/feltdb/tests/workload_envelope_contract.rs +15 -2
- package/dist/create/server-source/crates/feltdb-server/Cargo.toml +4 -0
- package/dist/create/server-source/crates/feltdb-server/benches/query_shape_observer.rs +136 -0
- package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +3 -0
- package/dist/create/server-source/crates/feltdb-server/src/lib.rs +1 -0
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +74 -5
- package/dist/create/server-source/crates/feltdb-server/src/query_shape.rs +1137 -0
- package/dist/create/server-source/crates/feltdb-server/tests/fixtures/query-shape-trace.expected.json +300 -0
- package/dist/create/server-source/crates/feltdb-server/tests/fixtures/query-shape-trace.json +25 -0
- package/dist/create/server-source/crates/feltdb-server/tests/query_shape_trace.rs +128 -0
- package/dist/index-core.d.ts +1 -0
- package/dist/index-core.d.ts.map +1 -1
- package/dist/index-core.js +1 -0
- package/dist/studio/app.d.ts.map +1 -1
- package/dist/studio/components/GlobalSearch.d.ts.map +1 -1
- package/dist/studio/components/index.js +1 -1
- package/dist/studio/{components-BHTKlUgT.js → components-Bivm2lX6.js} +2 -1
- package/dist/studio/control-plane/Agents.d.ts.map +1 -1
- package/dist/studio/control-plane/Data.d.ts.map +1 -1
- package/dist/studio/control-plane/Operations.d.ts.map +1 -1
- package/dist/studio/control-plane/Overview.d.ts.map +1 -1
- package/dist/studio/control-plane/Schema.d.ts.map +1 -1
- package/dist/studio/control-plane/State.d.ts +11 -0
- package/dist/studio/control-plane/State.d.ts.map +1 -1
- package/dist/studio/control-plane/index.d.ts +1 -1
- package/dist/studio/control-plane/index.d.ts.map +1 -1
- package/dist/studio/control-plane/index.js +2 -2
- package/dist/studio/control-plane/primitives.d.ts +14 -2
- package/dist/studio/control-plane/primitives.d.ts.map +1 -1
- package/dist/studio/control-plane-CLm69XwX.js +4121 -0
- package/dist/studio/index.js +266 -235
- package/dist/studio/studio.css +1 -1
- package/dist/studio-app/assets/{feltdb_wasm-DYfWAV0E.js → feltdb_wasm-DACcD-zs.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-BejThWID.wasm +0 -0
- package/dist/studio-app/assets/index-CiC8VY-a.css +1 -0
- package/dist/studio-app/assets/index-CoZaxnlm.js +30 -0
- package/dist/studio-app/index.html +2 -2
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/package.json +2 -2
- package/dist/studio/control-plane-DP8ljTDO.js +0 -3207
- package/dist/studio-app/assets/feltdb_wasm_bg-DHiOUhoQ.wasm +0 -0
- package/dist/studio-app/assets/index-6f6KVx5R.js +0 -30
- package/dist/studio-app/assets/index-BrsF-kWL.css +0 -1
|
@@ -51,6 +51,11 @@ struct Workload {
|
|
|
51
51
|
struct Measurement {
|
|
52
52
|
elapsed: Duration,
|
|
53
53
|
log_bytes: u64,
|
|
54
|
+
/// `log_bytes` without this database's instance id. Every record carries
|
|
55
|
+
/// that id, and it is derived from the log's path and printed in unpadded
|
|
56
|
+
/// hex, so two logs at different paths can differ in size by a few bytes
|
|
57
|
+
/// per record while holding identical databases.
|
|
58
|
+
identity_free_log_bytes: u64,
|
|
54
59
|
revisions: usize,
|
|
55
60
|
}
|
|
56
61
|
|
|
@@ -93,9 +98,14 @@ fn run(directory: &Path, workload: Workload, mode: DurabilityMode) -> Measuremen
|
|
|
93
98
|
.map(|resource| store.history_of(&format!("bench:{resource}")).len())
|
|
94
99
|
.sum();
|
|
95
100
|
|
|
101
|
+
let log = std::fs::read_to_string(&path).unwrap();
|
|
102
|
+
let instance_id = db.instance_id().unwrap();
|
|
103
|
+
let identity_bytes = log.matches(instance_id.as_str()).count() * instance_id.len();
|
|
104
|
+
|
|
96
105
|
Measurement {
|
|
97
106
|
elapsed,
|
|
98
|
-
log_bytes:
|
|
107
|
+
log_bytes: log.len() as u64,
|
|
108
|
+
identity_free_log_bytes: (log.len() - identity_bytes) as u64,
|
|
99
109
|
revisions,
|
|
100
110
|
}
|
|
101
111
|
}
|
|
@@ -217,7 +227,10 @@ fn a_barrier_per_write_costs_more_than_none() {
|
|
|
217
227
|
|
|
218
228
|
// Durability is the only thing that changed. The database is identical.
|
|
219
229
|
assert_eq!(flushed.revisions, synced.revisions);
|
|
220
|
-
assert_eq!(
|
|
230
|
+
assert_eq!(
|
|
231
|
+
flushed.identity_free_log_bytes,
|
|
232
|
+
synced.identity_free_log_bytes
|
|
233
|
+
);
|
|
221
234
|
}
|
|
222
235
|
|
|
223
236
|
/// Changing the mode changes the contract and nothing else: the same writes
|
|
@@ -23,3 +23,7 @@ tower-http = { version = "0.6", features = ["cors", "limit", "trace"] }
|
|
|
23
23
|
tracing = "0.1"
|
|
24
24
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
|
25
25
|
uuid = { version = "1.6", features = ["v4"] }
|
|
26
|
+
|
|
27
|
+
[[bench]]
|
|
28
|
+
name = "query_shape_observer"
|
|
29
|
+
harness = false
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
//! Direct per-operation cost of the query-shape observer.
|
|
2
|
+
//!
|
|
3
|
+
//! This isolates what the observer adds to one `POST /query`: building the
|
|
4
|
+
//! structural input, normalizing, and recording one outcome. It is deliberately
|
|
5
|
+
//! not an end-to-end number — an HTTP round trip costs orders of magnitude more
|
|
6
|
+
//! and would hide it — and it says nothing about contention with the database's
|
|
7
|
+
//! state lock, which the observer never takes. The end-to-end comparison lives
|
|
8
|
+
//! in `tools/benchmarks/query-shape-observer.mjs`.
|
|
9
|
+
//!
|
|
10
|
+
//! cargo bench --locked -p feltdb-server --bench query_shape_observer
|
|
11
|
+
//!
|
|
12
|
+
//! Output is one JSON object. Each figure is the median of `ROUNDS` timed
|
|
13
|
+
//! rounds, in nanoseconds per operation, on this machine and build profile
|
|
14
|
+
//! only.
|
|
15
|
+
|
|
16
|
+
use feltdb_server::query_shape::{Access, ObserverConfig, QueryShapeObserver, ShapeInput};
|
|
17
|
+
use std::hint::black_box;
|
|
18
|
+
use std::sync::Arc;
|
|
19
|
+
use std::time::Instant;
|
|
20
|
+
|
|
21
|
+
const ROUNDS: usize = 7;
|
|
22
|
+
const OPERATIONS: usize = 200_000;
|
|
23
|
+
|
|
24
|
+
fn median(mut values: Vec<f64>) -> f64 {
|
|
25
|
+
values.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
|
26
|
+
values[values.len() / 2]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// One simulated query: the same `Vec` construction the handler performs.
|
|
30
|
+
fn observe(observer: &QueryShapeObserver, collection: &str, field: &str) {
|
|
31
|
+
let predicates = vec![(field, "eq"), ("kind", "eq")];
|
|
32
|
+
let order_by = vec![("n", "asc")];
|
|
33
|
+
let observation = observer.begin(&ShapeInput {
|
|
34
|
+
collection,
|
|
35
|
+
predicates: &predicates,
|
|
36
|
+
order_by: &order_by,
|
|
37
|
+
limit: 50,
|
|
38
|
+
has_semantic_clause: false,
|
|
39
|
+
});
|
|
40
|
+
observation.finish_executed(Access::Indexed, black_box(40), black_box(40));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fn per_op(mut run: impl FnMut(usize)) -> f64 {
|
|
44
|
+
median(
|
|
45
|
+
(0..ROUNDS)
|
|
46
|
+
.map(|_| {
|
|
47
|
+
let started = Instant::now();
|
|
48
|
+
for index in 0..OPERATIONS {
|
|
49
|
+
run(index);
|
|
50
|
+
}
|
|
51
|
+
started.elapsed().as_nanos() as f64 / OPERATIONS as f64
|
|
52
|
+
})
|
|
53
|
+
.collect(),
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn main() {
|
|
58
|
+
let observer = QueryShapeObserver::new(ObserverConfig::with_capacity(128));
|
|
59
|
+
|
|
60
|
+
// The cost of building the input with no observer at all: the floor.
|
|
61
|
+
let floor = per_op(|_| {
|
|
62
|
+
let predicates = vec![("tenant", "eq"), ("kind", "eq")];
|
|
63
|
+
let order_by = vec![("n", "asc")];
|
|
64
|
+
black_box((&predicates, &order_by));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
let one_shape = per_op(|_| observe(&observer, "orders", "tenant"));
|
|
68
|
+
|
|
69
|
+
let fixed: Vec<String> = (0..64).map(|i| format!("c{i}")).collect();
|
|
70
|
+
let sixty_four_shapes = per_op(|index| observe(&observer, &fixed[index % 64], "tenant"));
|
|
71
|
+
|
|
72
|
+
// Every operation admits a new shape into a full table: the eviction scan.
|
|
73
|
+
let churn_names: Vec<String> = (0..4096).map(|i| format!("churn{i}")).collect();
|
|
74
|
+
let churn = QueryShapeObserver::new(ObserverConfig::with_capacity(128));
|
|
75
|
+
let eviction_churn = per_op(|index| observe(&churn, &churn_names[index % 4096], "tenant"));
|
|
76
|
+
let churn_max = QueryShapeObserver::new(ObserverConfig::with_capacity(1024));
|
|
77
|
+
let eviction_churn_max_capacity =
|
|
78
|
+
per_op(|index| observe(&churn_max, &churn_names[index % 4096], "tenant"));
|
|
79
|
+
|
|
80
|
+
let reserved = per_op(|_| {
|
|
81
|
+
let order_by = vec![("n", "asc")];
|
|
82
|
+
black_box(observer.begin(&ShapeInput {
|
|
83
|
+
collection: "_feltdb.Proposal",
|
|
84
|
+
predicates: &[],
|
|
85
|
+
order_by: &order_by,
|
|
86
|
+
limit: 50,
|
|
87
|
+
has_semantic_clause: false,
|
|
88
|
+
}));
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Contention: four threads recording into one observer.
|
|
92
|
+
let shared = Arc::new(QueryShapeObserver::new(ObserverConfig::with_capacity(128)));
|
|
93
|
+
let contended = median(
|
|
94
|
+
(0..ROUNDS)
|
|
95
|
+
.map(|_| {
|
|
96
|
+
let started = Instant::now();
|
|
97
|
+
let handles: Vec<_> = (0..4)
|
|
98
|
+
.map(|thread| {
|
|
99
|
+
let shared = shared.clone();
|
|
100
|
+
std::thread::spawn(move || {
|
|
101
|
+
let name = format!("t{thread}");
|
|
102
|
+
for _ in 0..OPERATIONS / 4 {
|
|
103
|
+
observe(&shared, &name, "tenant");
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
.collect();
|
|
108
|
+
for handle in handles {
|
|
109
|
+
handle.join().unwrap();
|
|
110
|
+
}
|
|
111
|
+
started.elapsed().as_nanos() as f64 / OPERATIONS as f64
|
|
112
|
+
})
|
|
113
|
+
.collect(),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
let snapshot = churn_max.snapshot();
|
|
117
|
+
println!(
|
|
118
|
+
"{}",
|
|
119
|
+
serde_json::json!({
|
|
120
|
+
"benchmark": "query_shape_observer",
|
|
121
|
+
"profile": if cfg!(debug_assertions) { "debug" } else { "release" },
|
|
122
|
+
"operations_per_round": OPERATIONS,
|
|
123
|
+
"rounds": ROUNDS,
|
|
124
|
+
"unit": "ns/op (median of rounds)",
|
|
125
|
+
"input_construction_floor_no_observer": floor,
|
|
126
|
+
"begin_finish_one_shape": one_shape,
|
|
127
|
+
"begin_finish_64_shapes": sixty_four_shapes,
|
|
128
|
+
"begin_finish_every_op_evicts_capacity_128": eviction_churn,
|
|
129
|
+
"begin_finish_every_op_evicts_capacity_1024": eviction_churn_max_capacity,
|
|
130
|
+
"reserved_collection_skipped": reserved,
|
|
131
|
+
"four_threads_one_observer_wall_per_op": contended,
|
|
132
|
+
"capacity_1024_memory_bound_bytes": snapshot.memory_bound_bytes,
|
|
133
|
+
"capacity_1024_memory_estimate_bytes_at_full": snapshot.memory_estimate_bytes,
|
|
134
|
+
})
|
|
135
|
+
);
|
|
136
|
+
}
|
|
@@ -86,6 +86,9 @@ pub struct AppState {
|
|
|
86
86
|
pub state_triggers: Arc<std::sync::Mutex<StateTriggerStore>>,
|
|
87
87
|
pub mesh: Arc<std::sync::Mutex<WorkerMeshStore>>,
|
|
88
88
|
pub readiness_probe: Arc<PathBuf>,
|
|
89
|
+
/// Opt-in, value-free query-shape observer for `POST /query`, or `None`
|
|
90
|
+
/// (the default). Diagnostic only: nothing reads it to answer a request.
|
|
91
|
+
pub query_shapes: Option<Arc<crate::query_shape::QueryShapeObserver>>,
|
|
89
92
|
pub bounded_query_cursors: Arc<std::sync::Mutex<HashMap<String, BoundedQueryCursor>>>,
|
|
90
93
|
/// The deployment's semantic runtime binding for bounded semantic query
|
|
91
94
|
/// composition, or `None` when the deployment configures no runtime. A
|
|
@@ -96,6 +96,7 @@ use feltdb_server::{
|
|
|
96
96
|
portable_bundle::{BundleArtifact, BundleStore, ExportOptions, ImportMode, PortableBundle},
|
|
97
97
|
principals::{PrincipalKind, PrincipalStatus},
|
|
98
98
|
providers::{ProviderManifest, ProviderStatus, ProviderStore},
|
|
99
|
+
query_shape::{Access, ObserverConfig, QueryShapeObserver, ShapeInput},
|
|
99
100
|
releases::{
|
|
100
101
|
provider_for, AuthorizedDeploymentContext, DeploymentPlan, DeploymentProfile,
|
|
101
102
|
DeploymentProvider, DeploymentProviderKind, DomainBinding, ReleaseManifest, ReleaseStore,
|
|
@@ -8766,6 +8767,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
8766
8767
|
// Constructed before the state so the same handle can be stopped after
|
|
8767
8768
|
// serving: the pipeline outlives the router by exactly one shutdown.
|
|
8768
8769
|
let audit_handle = AuditLog::new(config.audit.clone());
|
|
8770
|
+
// Opt-in and off by default. A malformed setting is a startup error rather
|
|
8771
|
+
// than a silent default, so a mistyped exclusion list cannot yield an
|
|
8772
|
+
// observer that records what the operator meant to exclude.
|
|
8773
|
+
let query_shapes = ObserverConfig::from_environment()?.map(|config| {
|
|
8774
|
+
println!(
|
|
8775
|
+
"feltdb: query-shape observer enabled (capacity {}, value-free, in-memory)",
|
|
8776
|
+
config.capacity
|
|
8777
|
+
);
|
|
8778
|
+
Arc::new(QueryShapeObserver::new(config))
|
|
8779
|
+
});
|
|
8769
8780
|
let state = AppState {
|
|
8770
8781
|
started_at: Instant::now(),
|
|
8771
8782
|
admission: Arc::new(tokio::sync::Semaphore::new(config.max_in_flight)),
|
|
@@ -8829,6 +8840,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
8829
8840
|
state_triggers: Arc::new(std::sync::Mutex::new(state_trigger_store)),
|
|
8830
8841
|
mesh: Arc::new(std::sync::Mutex::new(mesh_store)),
|
|
8831
8842
|
readiness_probe: Arc::new(readiness_probe),
|
|
8843
|
+
query_shapes,
|
|
8832
8844
|
bounded_query_cursors: Arc::new(std::sync::Mutex::new(HashMap::new())),
|
|
8833
8845
|
semantic_runtime: configured_semantic_runtime()?,
|
|
8834
8846
|
};
|
|
@@ -13520,7 +13532,7 @@ fn bounded_query_records(
|
|
|
13520
13532
|
state: &AppState,
|
|
13521
13533
|
request: &BoundedQueryRequest,
|
|
13522
13534
|
execution: BoundedQueryExecution,
|
|
13523
|
-
) -> Result<Vec<feltdb::StoredRow>, ApiError> {
|
|
13535
|
+
) -> Result<(Vec<feltdb::StoredRow>, Access), ApiError> {
|
|
13524
13536
|
if execution != BoundedQueryExecution::Scan {
|
|
13525
13537
|
let equalities = bounded_query_equalities(&request.conditions);
|
|
13526
13538
|
let indexed =
|
|
@@ -13530,7 +13542,7 @@ fn bounded_query_records(
|
|
|
13530
13542
|
bounded_query_matches(row, &request.conditions)
|
|
13531
13543
|
})?;
|
|
13532
13544
|
if let Some(records) = indexed {
|
|
13533
|
-
return Ok(records);
|
|
13545
|
+
return Ok((records, Access::Indexed));
|
|
13534
13546
|
}
|
|
13535
13547
|
if execution == BoundedQueryExecution::Index {
|
|
13536
13548
|
// Only reachable through the test-only header. A forced-index run
|
|
@@ -13541,11 +13553,12 @@ fn bounded_query_records(
|
|
|
13541
13553
|
));
|
|
13542
13554
|
}
|
|
13543
13555
|
}
|
|
13544
|
-
|
|
13556
|
+
let scanned = state
|
|
13545
13557
|
.db
|
|
13546
13558
|
.query_collection(&request.collection, None, |row| {
|
|
13547
13559
|
bounded_query_matches(row, &request.conditions)
|
|
13548
|
-
})
|
|
13560
|
+
})?;
|
|
13561
|
+
Ok((scanned, Access::Scan))
|
|
13549
13562
|
}
|
|
13550
13563
|
|
|
13551
13564
|
fn issue_bounded_cursor(
|
|
@@ -13628,6 +13641,37 @@ async fn execute_bounded_query(
|
|
|
13628
13641
|
feltdb::validate_semantic_clause(clause).map_err(semantic_query_error)?;
|
|
13629
13642
|
}
|
|
13630
13643
|
let hash = bounded_query_hash(&request)?;
|
|
13644
|
+
// Diagnostic only, and only when opted in. The observation sees structure
|
|
13645
|
+
// (names, operators, directions), never a value, and any failure inside it
|
|
13646
|
+
// is its own: nothing below reads it to decide a result. Dropped without
|
|
13647
|
+
// being finished — an early `?` return, a cancelled request — it records an
|
|
13648
|
+
// error outcome for the shape.
|
|
13649
|
+
let observation = state.query_shapes.as_deref().map(|observer| {
|
|
13650
|
+
let predicates: Vec<(&str, &str)> = request
|
|
13651
|
+
.conditions
|
|
13652
|
+
.iter()
|
|
13653
|
+
.filter_map(|condition| {
|
|
13654
|
+
condition
|
|
13655
|
+
.operators
|
|
13656
|
+
.keys()
|
|
13657
|
+
.next()
|
|
13658
|
+
.map(|operator| (condition.field.as_str(), operator.as_str()))
|
|
13659
|
+
})
|
|
13660
|
+
.collect();
|
|
13661
|
+
let order_by: Vec<(&str, &str)> = request
|
|
13662
|
+
.order_by
|
|
13663
|
+
.iter()
|
|
13664
|
+
.map(|order| (order.field.as_str(), order.direction.as_str()))
|
|
13665
|
+
.collect();
|
|
13666
|
+
observer.begin(&ShapeInput {
|
|
13667
|
+
collection: &request.collection,
|
|
13668
|
+
predicates: &predicates,
|
|
13669
|
+
order_by: &order_by,
|
|
13670
|
+
limit: request.limit,
|
|
13671
|
+
has_semantic_clause: request.semantic.is_some(),
|
|
13672
|
+
})
|
|
13673
|
+
});
|
|
13674
|
+
let mut executed: Option<(Access, usize)> = None;
|
|
13631
13675
|
let (records, position) = if let Some(token) = &request.cursor {
|
|
13632
13676
|
let cursor = state
|
|
13633
13677
|
.bounded_query_cursors
|
|
@@ -13665,7 +13709,8 @@ async fn execute_bounded_query(
|
|
|
13665
13709
|
// this surface's contract, so the matching set — not the collection, and
|
|
13666
13710
|
// not an index bucket — is what gets sorted, and the limit is applied to
|
|
13667
13711
|
// that ordered set. Nothing below this line knows which execution ran.
|
|
13668
|
-
let matches = bounded_query_records(&state, &request, execution)?;
|
|
13712
|
+
let (matches, access) = bounded_query_records(&state, &request, execution)?;
|
|
13713
|
+
executed = Some((access, matches.len()));
|
|
13669
13714
|
// Result materialization happens outside the state lock, so it is a cost
|
|
13670
13715
|
// this request pays alone rather than one it imposes on every other
|
|
13671
13716
|
// operation. PR36 measures it separately for exactly that reason.
|
|
@@ -13724,6 +13769,12 @@ async fn execute_bounded_query(
|
|
|
13724
13769
|
} else {
|
|
13725
13770
|
None
|
|
13726
13771
|
};
|
|
13772
|
+
if let Some(observation) = observation {
|
|
13773
|
+
match executed {
|
|
13774
|
+
Some((access, matched)) => observation.finish_executed(access, matched, page.len()),
|
|
13775
|
+
None => observation.finish_continuation(page.len()),
|
|
13776
|
+
}
|
|
13777
|
+
}
|
|
13727
13778
|
// The semantic stage transforms exactly this page. It runs after the
|
|
13728
13779
|
// deterministic cursor is issued because continuation is by deterministic
|
|
13729
13780
|
// position: the page may return fewer records than `limit` and still not
|
|
@@ -13814,6 +13865,15 @@ fn query_execution_diagnostics_router() -> Router<AppState> {
|
|
|
13814
13865
|
get(query_execution_diagnostics),
|
|
13815
13866
|
);
|
|
13816
13867
|
}
|
|
13868
|
+
// Query-shape observations name collections and fields, so they are a
|
|
13869
|
+
// separate switch from the counters and exist only when the observer was
|
|
13870
|
+
// opted into. Counts and structure only; never a value.
|
|
13871
|
+
if ObserverConfig::from_environment().ok().flatten().is_some() {
|
|
13872
|
+
router = router.route(
|
|
13873
|
+
"/internal/query-shape-observations",
|
|
13874
|
+
get(query_shape_observations),
|
|
13875
|
+
);
|
|
13876
|
+
}
|
|
13817
13877
|
// Phase attribution is a separate switch from the execution counters,
|
|
13818
13878
|
// because it is a separate cost: the counters are a handful of atomic adds
|
|
13819
13879
|
// per query, while attribution reads the clock at every instrumented
|
|
@@ -13833,6 +13893,15 @@ fn query_execution_diagnostics_router() -> Router<AppState> {
|
|
|
13833
13893
|
router
|
|
13834
13894
|
}
|
|
13835
13895
|
|
|
13896
|
+
/// The observer's retained shapes and counters. Diagnostic evidence about
|
|
13897
|
+
/// workload structure, not proof that any change would help.
|
|
13898
|
+
async fn query_shape_observations(State(state): State<AppState>) -> Json<Value> {
|
|
13899
|
+
match state.query_shapes.as_deref() {
|
|
13900
|
+
Some(observer) => Json(json!({ "enabled": true, "observations": observer.snapshot() })),
|
|
13901
|
+
None => Json(json!({ "enabled": false })),
|
|
13902
|
+
}
|
|
13903
|
+
}
|
|
13904
|
+
|
|
13836
13905
|
/// Phase attribution: where elapsed time went, as counts and nanoseconds.
|
|
13837
13906
|
///
|
|
13838
13907
|
/// Test instrumentation, mounted only under `FELTDB_WORKLOAD_DIAGNOSTICS=1`.
|