Skip to content

BE-868: Set up modular REST APIs and shared OpenAPI documentation - #9770

Open
TimDiekmann wants to merge 10 commits into
mainfrom
t/be-868-modular-rest-apis
Open

TimDiekmann wants to merge 10 commits into
mainfrom
t/be-868-modular-rest-apis

Conversation

@TimDiekmann

@TimDiekmann TimDiekmann commented Sep 18, 2026

Copy link
Copy Markdown
Member

🌟 What is the purpose of this PR?

Establish separate Graph REST APIs for Entities v1, Types v1, and Internal. Each API owns its routes and OpenAPI document, with shared documentation available through self-hosted Scalar at /.

🔗 Related links

🚫 Blocked by

None.

🔍 What does this change?

  • Make rest::router the Graph HTTP entrypoint, with legacy handlers under rest::legacy and shared authentication, middleware, telemetry, and probes owned by rest.
  • Add /entities/v1, /types/v1, and /internal modules with optional and authenticated caller endpoints.
  • Generate OpenAPI documents with Aide and Schemars, reusable Problem Details responses, and public descriptions for actor identifiers.
  • Accept delegation and Cloudflare credentials in public APIs; additionally accept session credentials in Internal and Legacy.
  • Give each API separate actor and anonymous rate-limit budgets with shared CLI defaults. A single outer IP gate shares the address budget and records global gate metrics; principal metrics remain scoped to each API.
  • Serve Scalar and its JavaScript bundle locally, remove the API-reference toggle, and include the legacy document in the viewer.

Pre-Merge Checklist 🚀

🚢 Has this modified a publishable library?

This PR:

  • does not modify any publishable blocks or libraries, or modifications do not need publishing

📜 Does this require a change to the docs?

The changes in this PR:

  • require changes to docs which are made as part of this PR

🕸️ Does this require a change to the Turbo Graph?

The changes in this PR:

  • affect the execution graph; generated package dependency metadata has been updated.

⚠️ Known issues

The APIs currently contain caller endpoints for exercising authentication and documentation. Public authentication uses delegation and Cloudflare while OAuth is developed. Scalar's bundled JavaScript receives a checked patch for its optional-authentication indicator.

🐾 Next steps

Add the first entity and type operations, OAuth authentication, and an internal API versioning policy.

🛡 What tests cover this?

  • Shared caller tests cover public/internal credential selection, anonymous callers, required authentication, and provider failures.
  • Middleware tests cover independent actor and anonymous budgets, the shared IP budget, metrics, and limiter ownership. Graph composition tests cover provider selection for Legacy, per-module principal budgets, shared IP accounting, and documentation, fallback, and health routes.
  • JSON snapshots cover all three OpenAPI documents. Documentation tests check document routing and self-hosted Scalar at /.
  • All 34 REST and OpenAPI tests pass after the router restructuring. Clippy passes for Graph API and the Graph application; the affected benchmark compiles. Rustdoc and the OpenAPI generator pass, with unchanged specifications and snapshots.

❓ How to test this?

  1. Start the Graph and open http://localhost:4000/.
  2. Switch between Entities v1, Types v1, Internal, and Legacy in Scalar.
  3. Call /caller and /authenticated-caller beneath each new API prefix. Anonymous calls return actor: null from /caller; /authenticated-caller returns a Problem Details error without an authenticated actor.
  4. Try session credentials against Public and Internal, then try service delegation using Authorization: HASH-Service <secret> together with X-Authenticated-User-Actor-Id.

📹 Demo

No recording attached. The local Scalar viewer at / provides the interactive example.

@TimDiekmann TimDiekmann self-assigned this Sep 18, 2026
@vercel

vercel Bot commented Sep 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

4 Skipped Deployments
Project Deployment Actions Updated
hash Ignored Ignored Preview Sep 18, 2026 9:26pm UTC
hashdotdesign-tokens Ignored Ignored Preview Sep 18, 2026 9:26pm UTC
petrinaut Skipped Skipped Sep 18, 2026 9:26pm UTC
petrinaut-docs Skipped Skipped Sep 18, 2026 9:26pm UTC

Request Review

@github-actions github-actions Bot added area/deps Relates to third-party dependencies (area) area/apps > hash* Affects HASH (a `hash-*` app) area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) type/eng > backend Owned by the @backend team area/tests New or updated tests area/apps area/apps > hash-graph labels Sep 18, 2026
@TimDiekmann
TimDiekmann marked this pull request as ready for review September 18, 2026 13:43
@TimDiekmann
TimDiekmann requested a review from a team as a code owner September 18, 2026 13:43
@cursor

cursor Bot commented Sep 18, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches authentication provider composition, rate-limit layering, and the main HTTP router assembly; behavior changes include always-on docs at / and shared budgets across legacy and new APIs.

Overview
Reorganizes Graph HTTP around a new rest module: rest::router becomes the server entrypoint, legacy handlers move under rest::legacy, and shared auth, rate limits, telemetry, and probes live at the rest layer.

Adds versioned Entities v1, Types v1, and Internal APIs (caller endpoints for now) with Aide/Schemars OpenAPI generation, RFC 9457 Problem Details in specs, and insta snapshots per API. Public APIs accept service delegation and Cloudflare Access; Internal and legacy also accept Kratos sessions, wired through separate provider chains and audience-specific credential documentation.

Replaces the optional CDN utoipa-scalar viewer and HASH_GRAPH_SERVE_API_REFERENCE with a self-hosted Scalar UI at /, per-API {prefix}/openapi.json, legacy spec still at /openapi.json, and an embedded overview.md in each document. Rate limiting is unified: one IP gate, shared actor/anonymous budgets across legacy and new routes, with 404s returning application/problem+json.

Supporting changes: type-system optional schemars on actor IDs, scalar_api_reference workspace dep, .dockerignore exception for overview markdown, and Atlas auth/rate-limit rejection types aligned with middleware.

Reviewed by Cursor Bugbot for commit 643cf52. Bugbot is set up for automated code reviews on this repo. Configure here.

@codecov

codecov Bot commented Sep 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.09735% with 191 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.82%. Comparing base (4f5ac34) to head (f70c717).
⚠️ Report is 7 commits behind head on main.

Files with missing lines Patch % Lines
libs/@local/graph/api/src/rest/router.rs 0.00% 56 Missing ⚠️
libs/@local/graph/api/src/rest/authentication.rs 0.00% 34 Missing ⚠️
libs/@local/graph/api/src/rest/rate_limit.rs 0.00% 21 Missing ⚠️
libs/@local/graph/api/src/rest/openapi/mod.rs 87.50% 9 Missing and 8 partials ⚠️
apps/hash-graph/src/subcommand/atlas.rs 0.00% 9 Missing ⚠️
libs/@local/middleware/src/authentication/mod.rs 78.37% 7 Missing and 1 partial ⚠️
libs/@local/graph/api/src/rest/legacy/mod.rs 0.00% 7 Missing ⚠️
...s/@local/middleware/src/authentication/provider.rs 0.00% 6 Missing ⚠️
libs/@local/middleware/src/rate_limit/mod.rs 94.00% 5 Missing and 1 partial ⚠️
apps/hash-graph/src/subcommand/admin_server.rs 0.00% 5 Missing ⚠️
... and 9 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9770      +/-   ##
==========================================
+ Coverage   66.58%   66.82%   +0.23%     
==========================================
  Files        1805     1871      +66     
  Lines      191004   196421    +5417     
  Branches     7858     7988     +130     
==========================================
+ Hits       127184   131260    +4076     
- Misses      62323    63621    +1298     
- Partials     1497     1540      +43     
Flag Coverage Δ
apps.hash-ai-worker-ts 1.99% <ø> (ø)
apps.hash-api 15.35% <ø> (ø)
apps.hash-graph 12.25% <0.00%> (-1.38%) ⬇️
local.hash-backend-utils 3.27% <ø> (ø)
local.hash-graph-sdk 10.02% <ø> (ø)
local.hash-isomorphic-utils 12.22% <ø> (ø)
rust.hash-graph-api 32.20% <81.37%> (+13.44%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@vercel
vercel Bot temporarily deployed to Preview – petrinaut-docs September 18, 2026 13:47 Inactive
@vercel
vercel Bot temporarily deployed to Preview – petrinaut September 18, 2026 13:47 Inactive
@codspeed

codspeed Bot commented Sep 18, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

⚠️ 6 benchmarks measured no execution time

Nothing ran under measurement, usually because the compiler removed the code under test. These results are not comparable, so they count as unchanged.

Preventing compiler optimizations

✅ 98 untouched benchmarks

Performance Changes

Benchmark BASE HEAD Efficiency
⚠️ as_constant < 1 ns < 1 ns N/A
⚠️ constant_equal < 1 ns < 1 ns N/A
⚠️ constant_not_equal < 1 ns < 1 ns N/A
⚠️ access < 1 ns < 1 ns N/A
⚠️ runtime_equal < 1 ns < 1 ns N/A
⚠️ runtime_not_equal < 1 ns < 1 ns N/A

Comparing t/be-868-modular-rest-apis (643cf52) with main (3dba8b8)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (adfb316) during the generation of this report, so 3dba8b8 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@vercel
vercel Bot temporarily deployed to Preview – petrinaut-docs September 18, 2026 14:22 Inactive
@vercel
vercel Bot temporarily deployed to Preview – petrinaut September 18, 2026 14:22 Inactive
Comment thread libs/@local/graph/api/src/rest/mod.rs Outdated
probe, rate_limit, telemetry,
};

pub struct Dependencies<S>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

non-blocking: longer term we should really move away from Dependencies via Extension to just using state with https://docs.rs/axum/latest/axum/extract/trait.FromRef.html (State and FromRef were introduced after we had our first version) but it makes it possible to have things be actually type checked that their data is there.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I remember when they added it, but I never got to it. I might just do it as follow-up for the new endpoints.

prefix: &'static str,
audience: Audience,
info: Info,
create_routes: impl FnOnce() -> ApiRouter,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why FnOnce here? why not give it the ApiRouter directly?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Because the routes have to be generated after build registers aide's on_error and inside the context that this finish_api drains.
If we'd pass an ApiRouter directly, this would not see the thread-local on_error. I was thinking about using fn() -> ApiRouter to prevent || prebuilt, but I found that to be overengineered.

Comment on lines +59 to +133
pub(super) fn problem_response(status: u16, description: &str) -> Response {
// Aide's shared generator uses the deserialization contract. Responses need the
// serialization contract; inline subschemas keep this generator's references local.
let mut generator = SchemaSettings::draft07()
.for_serialize()
.with(|settings| settings.inline_subschemas = true)
.into_generator();
let mut schema = generator.subschema_for::<ProblemDetails<'static>>();
let status_schema = schema
.pointer_mut("/properties/status")
.and_then(serde_json::Value::as_object_mut)
.expect("the problem schema should contain an object schema for status");
status_schema
.retain(|key, _| !matches!(key.as_str(), "examples" | "format" | "minimum" | "maximum"));
status_schema.insert("const".to_owned(), status.into());
status_schema.insert("examples".to_owned(), [status].into());

if let Ok(status_code) = http::StatusCode::from_u16(status)
&& let Some(reason) = status_code.canonical_reason()
{
schema
.pointer_mut("/properties/title")
.and_then(serde_json::Value::as_object_mut)
.expect("the problem schema should contain an object schema for title")
.insert("examples".to_owned(), [reason].into());

let slug = reason.replace('\'', "").to_case(Case::Kebab);
let type_uri = format!("https://example.com/problems/{slug}");
schema
.pointer_mut("/properties/type")
.and_then(serde_json::Value::as_object_mut)
.expect("the problem schema should contain an object schema for type")
.insert("examples".to_owned(), [type_uri].into());
}

Response {
description: description.to_owned(),
content: IndexMap::from_iter([(
"application/problem+json".to_owned(),
MediaType {
schema: Some(SchemaObject {
json_schema: schema,
example: None,
external_docs: None,
}),
..MediaType::default()
},
)]),
..Response::default()
}
}

pub(super) fn add_response(api: &mut OpenApi, status: u16, name: &str, response: Response) {
api.components
.get_or_insert_with(Default::default)
.responses
.insert(name.to_owned(), ReferenceOr::Item(response));

let Some(paths) = &mut api.paths else {
return;
};
for path in paths.paths.values_mut() {
let Some(path) = path.as_item_mut() else {
continue;
};
for (_, operation) in iter_operations_mut(path) {
operation
.responses
.get_or_insert_with(Default::default)
.responses
.entry(StatusCode::Code(status))
.or_insert_with(|| ReferenceOr::ref_(&format!("#/components/responses/{name}")));
}
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I am a bit puzzled by this why the need for this? isn't this the exact case for OperationOutput and TransformOutput? This is what I use:

impl OperationOutput for Problem<'_> {
type Inner = Self;
fn operation_response(
ctx: &mut GenContext,
_operation: &mut openapi::Operation,
) -> Option<openapi::Response> {
let json_schema = ctx.schema.subschema_for::<Problem<'static>>();
let mut response = openapi::Response {
description: "an RFC 9457 problem document".into(),
..Default::default()
};
response.content.insert(
"application/problem+json".into(),
openapi::MediaType {
schema: Some(openapi::SchemaObject {
json_schema,
example: None,
external_docs: None,
}),
..Default::default()
},
);
Some(response)
}
/// Answers with a single default response rather than one per status code.
///
/// A [`Problem`] document carries its own status. No route fixes one in advance.
fn inferred_responses(
ctx: &mut GenContext,
operation: &mut openapi::Operation,
) -> Vec<(Option<openapi::StatusCode>, openapi::Response)> {
let response = Self::operation_response(ctx, operation)
.unwrap_or_else(|| unreachable!("`operation_response` answers every operation"));
vec![(None, response)]
}
}
in conjunction with:
.response_with::<503, Problem<'static>, _>(|response| {
response.description("no generation is ready to serve requests")
})

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks, fixed!

Comment on lines +73 to +95
pub(super) fn responses(api: &mut OpenApi) {
for (status, name, description) in [
(
400,
"MalformedCredentials",
"Malformed credentials or actor header.",
),
(
401,
"AuthenticationRejected",
"The credentials cannot resolve to a permitted caller.",
),
(
503,
"AuthenticationUnavailable",
"The credential provider or actor store is unavailable.",
),
] {
add_response(api, status, name, problem_response(status, description));
}
}

pub(super) struct Actor<C>(ActorId, PhantomData<fn() -> C>);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I used .with and TransformOperation: https://github.com/hashintel/hash/blob/adfb31627ca77a9f8d0bc741c7e9b60b9921f666/libs/%40local/graph/atlas/src/api/clause.rs (https://docs.rs/aide/latest/aide/transform/struct.TransformOpenApi.html#method.with)

(note that the aide use rn is not ideal for errors in this case, something i am looking to remedy once we have aide inside all our other layers, needed to kinda paper over things as we didn't have aide support yet)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed.

}
}

pub(super) struct MaybeActor<C>(Option<ActorId>, PhantomData<fn() -> C>);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I guess we need this because Option only makes the parameters optional, not the security requirements: https://docs.rs/aide/latest/src/aide/impls/mod.rs.html#62-94

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, and with that we also implicitly can add the empty security requirement.

Comment thread libs/@local/graph/api/src/rest/rate_limit/config.rs Outdated
}
}

pub(super) fn responses(api: &mut OpenApi) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this is my version:

fn too_many_requests(operation: TransformOperation<'_>) -> TransformOperation<'_> {
operation.response_with::<429, Problem<'static>, _>(|mut response| {
response
.inner()
.headers
.insert("Retry-After".to_owned(), headers::retry_after());
response.description(
"`too-many-requests`: the caller is over its per-address or per-actor budget; \
`Retry-After` states whole seconds until it admits again",
)
})
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I had this before, I don't know why I lost this, and it annoys me. Fixed.

Comment thread libs/@local/graph/api/src/rest/middleware/mod.rs
Give `problematic` and `hash-middleware` an `aide` feature so the
authentication and rate-limit rejections document themselves as problem
responses, and let each Graph API pull them in through `TransformOpenApi`
clauses instead of hand-built response objects. Shared problem responses
move into `components/responses`, each with an example taken from the
runtime body.

Tie each API's audience, security schemes and route markers together
through a `Credentials` trait, and check after document generation that
every required scheme is declared. Legacy routes and every API draw on one
root `RateLimiters`; `with_principal_limits` stays as the override for
later.

Build the operator provider chain once and share it with the internal
chain, answer unmatched paths with a problem document, panic on aide
documentation defects while the routes register, and force the Scalar
bundle patch at startup.
The limiter behind authentication budgets anonymous requests by address
and actors across addresses, so `CallerLimitLayer`, `CallerRateLimitConfig`
and `with_caller_limits` take the name of the `Caller` the provider chain
resolves. A principal is an actor or an actor group and names neither
outcome. The `stage` attribute of `hash.rate_limit.unchecked` follows with
the value `caller`.
`SharedProviders` verify the credentials every API accepts, Cloudflare
Access and service delegation, matching `shared_schemes` in the credential
documentation. `SessionProviders` add the Kratos session in front of them
for the APIs that serve browser sessions.
@vercel
vercel Bot temporarily deployed to Preview – petrinaut September 18, 2026 20:03 Inactive
@vercel
vercel Bot temporarily deployed to Preview – petrinaut-docs September 18, 2026 20:05 Inactive
The provider chain used to consult the Cloudflare Access JWT ahead of
service delegation, so a request carrying both resolved to the identity
the edge stamped on it and never read the credential the caller presented
on purpose. The providers now form three groups, `ExplicitProviders`,
`SessionProviders` and `EnvironmentProviders`, which each router composes
in that order. Only the Cloudflare Access provider is shared between the
chains, for its JWKS cache.
@vercel
vercel Bot temporarily deployed to Preview – petrinaut September 18, 2026 20:24 Inactive
@vercel
vercel Bot temporarily deployed to Preview – petrinaut-docs September 18, 2026 20:24 Inactive
@vercel
vercel Bot temporarily deployed to Preview – petrinaut September 18, 2026 20:59 Inactive
@vercel
vercel Bot temporarily deployed to Preview – petrinaut-docs September 18, 2026 20:59 Inactive
@github-actions

Copy link
Copy Markdown
Contributor

Benchmark results

@rust/hash-graph-benches – Integrations

policy_resolution_large

Function Value Mean Flame graphs
resolve_policies_for_actor user: empty, selectivity: high, policies: 2002 $$20.1 \mathrm{ms} \pm 130 \mathrm{μs}\left({\color{gray}1.16 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: low, policies: 1 $$2.64 \mathrm{ms} \pm 13.4 \mathrm{μs}\left({\color{gray}-0.816 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: medium, policies: 1002 $$9.92 \mathrm{ms} \pm 85.4 \mathrm{μs}\left({\color{gray}-1.032 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: high, policies: 3314 $$34.0 \mathrm{ms} \pm 331 \mathrm{μs}\left({\color{gray}0.340 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: low, policies: 1 $$11.8 \mathrm{ms} \pm 97.5 \mathrm{μs}\left({\color{gray}0.497 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: medium, policies: 1527 $$19.6 \mathrm{ms} \pm 130 \mathrm{μs}\left({\color{gray}-1.449 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: high, policies: 2078 $$20.5 \mathrm{ms} \pm 144 \mathrm{μs}\left({\color{gray}0.612 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: low, policies: 1 $$2.86 \mathrm{ms} \pm 14.0 \mathrm{μs}\left({\color{gray}-0.217 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: medium, policies: 1033 $$10.4 \mathrm{ms} \pm 96.0 \mathrm{μs}\left({\color{gray}-2.827 \mathrm{\%}}\right) $$ Flame Graph

policy_resolution_medium

Function Value Mean Flame graphs
resolve_policies_for_actor user: empty, selectivity: high, policies: 102 $$2.87 \mathrm{ms} \pm 15.7 \mathrm{μs}\left({\color{gray}-0.253 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: low, policies: 1 $$2.35 \mathrm{ms} \pm 18.9 \mathrm{μs}\left({\color{gray}2.05 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: medium, policies: 52 $$2.64 \mathrm{ms} \pm 17.5 \mathrm{μs}\left({\color{gray}1.75 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: high, policies: 269 $$4.03 \mathrm{ms} \pm 29.0 \mathrm{μs}\left({\color{gray}2.06 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: low, policies: 1 $$2.77 \mathrm{ms} \pm 16.0 \mathrm{μs}\left({\color{gray}1.45 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: medium, policies: 108 $$3.21 \mathrm{ms} \pm 19.8 \mathrm{μs}\left({\color{gray}1.39 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: high, policies: 133 $$3.43 \mathrm{ms} \pm 22.2 \mathrm{μs}\left({\color{gray}1.06 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: low, policies: 1 $$2.64 \mathrm{ms} \pm 11.1 \mathrm{μs}\left({\color{gray}-0.971 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: medium, policies: 63 $$3.13 \mathrm{ms} \pm 17.3 \mathrm{μs}\left({\color{gray}-2.858 \mathrm{\%}}\right) $$ Flame Graph

policy_resolution_none

Function Value Mean Flame graphs
resolve_policies_for_actor user: empty, selectivity: high, policies: 2 $$2.11 \mathrm{ms} \pm 10.8 \mathrm{μs}\left({\color{gray}-0.189 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: low, policies: 1 $$2.07 \mathrm{ms} \pm 10.4 \mathrm{μs}\left({\color{gray}-0.535 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: medium, policies: 2 $$2.17 \mathrm{ms} \pm 10.3 \mathrm{μs}\left({\color{gray}-0.187 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: high, policies: 8 $$2.37 \mathrm{ms} \pm 12.5 \mathrm{μs}\left({\color{gray}0.414 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: low, policies: 1 $$2.21 \mathrm{ms} \pm 11.5 \mathrm{μs}\left({\color{gray}-2.028 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: medium, policies: 3 $$2.46 \mathrm{ms} \pm 11.9 \mathrm{μs}\left({\color{gray}1.28 \mathrm{\%}}\right) $$ Flame Graph

policy_resolution_small

Function Value Mean Flame graphs
resolve_policies_for_actor user: empty, selectivity: high, policies: 52 $$2.38 \mathrm{ms} \pm 13.2 \mathrm{μs}\left({\color{gray}0.414 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: low, policies: 1 $$2.14 \mathrm{ms} \pm 11.4 \mathrm{μs}\left({\color{gray}0.611 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: medium, policies: 26 $$2.28 \mathrm{ms} \pm 9.15 \mathrm{μs}\left({\color{gray}-0.915 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: high, policies: 94 $$2.71 \mathrm{ms} \pm 16.5 \mathrm{μs}\left({\color{gray}0.270 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: low, policies: 1 $$2.35 \mathrm{ms} \pm 13.2 \mathrm{μs}\left({\color{gray}-0.731 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: medium, policies: 27 $$2.59 \mathrm{ms} \pm 18.8 \mathrm{μs}\left({\color{gray}1.82 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: high, policies: 66 $$2.65 \mathrm{ms} \pm 19.2 \mathrm{μs}\left({\color{gray}-0.343 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: low, policies: 1 $$2.37 \mathrm{ms} \pm 14.0 \mathrm{μs}\left({\color{gray}1.30 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: medium, policies: 29 $$2.59 \mathrm{ms} \pm 19.3 \mathrm{μs}\left({\color{gray}-2.955 \mathrm{\%}}\right) $$ Flame Graph

read_scaling_complete

Function Value Mean Flame graphs
entity_by_id;one_depth 1 entities $$24.5 \mathrm{ms} \pm 126 \mathrm{μs}\left({\color{gray}0.507 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;one_depth 10 entities $$52.7 \mathrm{ms} \pm 395 \mathrm{μs}\left({\color{gray}-2.392 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;one_depth 25 entities $$27.5 \mathrm{ms} \pm 165 \mathrm{μs}\left({\color{gray}-1.408 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;one_depth 5 entities $$29.3 \mathrm{ms} \pm 150 \mathrm{μs}\left({\color{gray}-1.294 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;one_depth 50 entities $$34.7 \mathrm{ms} \pm 166 \mathrm{μs}\left({\color{red}5.87 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;two_depth 1 entities $$26.2 \mathrm{ms} \pm 170 \mathrm{μs}\left({\color{gray}1.51 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;two_depth 10 entities $$300 \mathrm{ms} \pm 606 \mathrm{μs}\left({\color{lightgreen}-5.946 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;two_depth 25 entities $$69.7 \mathrm{ms} \pm 418 \mathrm{μs}\left({\color{gray}-4.457 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;two_depth 5 entities $$54.4 \mathrm{ms} \pm 288 \mathrm{μs}\left({\color{gray}-3.142 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;two_depth 50 entities $$212 \mathrm{ms} \pm 879 \mathrm{μs}\left({\color{gray}-1.742 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;zero_depth 1 entities $$7.86 \mathrm{ms} \pm 37.3 \mathrm{μs}\left({\color{gray}-4.533 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;zero_depth 10 entities $$8.09 \mathrm{ms} \pm 49.8 \mathrm{μs}\left({\color{gray}-3.395 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;zero_depth 25 entities $$8.38 \mathrm{ms} \pm 67.0 \mathrm{μs}\left({\color{gray}-1.346 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;zero_depth 5 entities $$7.97 \mathrm{ms} \pm 45.4 \mathrm{μs}\left({\color{gray}-4.943 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;zero_depth 50 entities $$8.10 \mathrm{ms} \pm 57.8 \mathrm{μs}\left({\color{gray}-2.928 \mathrm{\%}}\right) $$ Flame Graph

read_scaling_linkless

Function Value Mean Flame graphs
entity_by_id 1 entities $$7.95 \mathrm{ms} \pm 50.1 \mathrm{μs}\left({\color{gray}-2.661 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id 10 entities $$7.88 \mathrm{ms} \pm 64.8 \mathrm{μs}\left({\color{gray}-1.930 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id 100 entities $$8.01 \mathrm{ms} \pm 47.9 \mathrm{μs}\left({\color{gray}-0.088 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id 1000 entities $$7.92 \mathrm{ms} \pm 36.1 \mathrm{μs}\left({\color{gray}0.534 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id 10000 entities $$8.30 \mathrm{ms} \pm 46.8 \mathrm{μs}\left({\color{gray}-0.744 \mathrm{\%}}\right) $$ Flame Graph

representative_read_entity

Function Value Mean Flame graphs
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/block/v/1 $$8.19 \mathrm{ms} \pm 40.1 \mathrm{μs}\left({\color{gray}-3.110 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/book/v/1 $$8.33 \mathrm{ms} \pm 48.3 \mathrm{μs}\left({\color{gray}0.314 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/building/v/1 $$8.37 \mathrm{ms} \pm 59.3 \mathrm{μs}\left({\color{gray}1.52 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/organization/v/1 $$8.38 \mathrm{ms} \pm 49.0 \mathrm{μs}\left({\color{gray}0.754 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/page/v/2 $$8.30 \mathrm{ms} \pm 49.1 \mathrm{μs}\left({\color{gray}-1.756 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/person/v/1 $$8.32 \mathrm{ms} \pm 57.8 \mathrm{μs}\left({\color{gray}-0.248 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/playlist/v/1 $$8.34 \mathrm{ms} \pm 77.5 \mathrm{μs}\left({\color{gray}-1.181 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/song/v/1 $$8.29 \mathrm{ms} \pm 38.1 \mathrm{μs}\left({\color{gray}0.811 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/uk-address/v/1 $$8.27 \mathrm{ms} \pm 44.5 \mathrm{μs}\left({\color{gray}-2.062 \mathrm{\%}}\right) $$ Flame Graph

representative_read_entity_type

Function Value Mean Flame graphs
get_entity_type_by_id Account ID: bf5a9ef5-dc3b-43cf-a291-6210c0321eba $$6.19 \mathrm{ms} \pm 38.2 \mathrm{μs}\left({\color{gray}2.16 \mathrm{\%}}\right) $$ Flame Graph

representative_read_multiple_entities

Function Value Mean Flame graphs
entity_by_property traversal_paths=0 0 $$48.1 \mathrm{ms} \pm 316 \mathrm{μs}\left({\color{red}9.68 \mathrm{\%}}\right) $$
entity_by_property traversal_paths=255 1,resolve_depths=inherit:1;values:255;properties:255;links:127;link_dests:126;type:true $$85.4 \mathrm{ms} \pm 703 \mathrm{μs}\left({\color{gray}-0.280 \mathrm{\%}}\right) $$
entity_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:0;properties:0;links:0;link_dests:0;type:false $$53.3 \mathrm{ms} \pm 346 \mathrm{μs}\left({\color{red}9.43 \mathrm{\%}}\right) $$
entity_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:0;properties:0;links:1;link_dests:0;type:true $$60.0 \mathrm{ms} \pm 419 \mathrm{μs}\left({\color{red}7.31 \mathrm{\%}}\right) $$
entity_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:0;properties:2;links:1;link_dests:0;type:true $$66.0 \mathrm{ms} \pm 344 \mathrm{μs}\left({\color{gray}1.94 \mathrm{\%}}\right) $$
entity_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:2;properties:2;links:1;link_dests:0;type:true $$70.0 \mathrm{ms} \pm 673 \mathrm{μs}\left({\color{gray}-1.569 \mathrm{\%}}\right) $$
link_by_source_by_property traversal_paths=0 0 $$35.2 \mathrm{ms} \pm 226 \mathrm{μs}\left({\color{gray}-2.996 \mathrm{\%}}\right) $$
link_by_source_by_property traversal_paths=255 1,resolve_depths=inherit:1;values:255;properties:255;links:127;link_dests:126;type:true $$56.7 \mathrm{ms} \pm 335 \mathrm{μs}\left({\color{gray}-0.941 \mathrm{\%}}\right) $$
link_by_source_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:0;properties:0;links:0;link_dests:0;type:false $$40.2 \mathrm{ms} \pm 197 \mathrm{μs}\left({\color{gray}-1.669 \mathrm{\%}}\right) $$
link_by_source_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:0;properties:0;links:1;link_dests:0;type:true $$46.3 \mathrm{ms} \pm 221 \mathrm{μs}\left({\color{lightgreen}-6.113 \mathrm{\%}}\right) $$
link_by_source_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:0;properties:2;links:1;link_dests:0;type:true $$48.4 \mathrm{ms} \pm 194 \mathrm{μs}\left({\color{gray}-2.203 \mathrm{\%}}\right) $$
link_by_source_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:2;properties:2;links:1;link_dests:0;type:true $$48.6 \mathrm{ms} \pm 239 \mathrm{μs}\left({\color{gray}-0.740 \mathrm{\%}}\right) $$

scenarios

Function Value Mean Flame graphs
full_test query-limited $$84.9 \mathrm{ms} \pm 469 \mathrm{μs}\left({\color{lightgreen}-10.926 \mathrm{\%}}\right) $$ Flame Graph
full_test query-unlimited $$93.1 \mathrm{ms} \pm 464 \mathrm{μs}\left({\color{lightgreen}-10.636 \mathrm{\%}}\right) $$ Flame Graph
linked_queries query-limited $$17.9 \mathrm{ms} \pm 96.3 \mathrm{μs}\left({\color{gray}-3.242 \mathrm{\%}}\right) $$ Flame Graph
linked_queries query-unlimited $$395 \mathrm{ms} \pm 653 \mathrm{μs}\left({\color{gray}-4.529 \mathrm{\%}}\right) $$ Flame Graph

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/apps > hash* Affects HASH (a `hash-*` app) area/apps > hash-graph area/apps area/deps Relates to third-party dependencies (area) area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) area/tests New or updated tests type/eng > backend Owned by the @backend team

Development

Successfully merging this pull request may close these issues.

2 participants