Skip to content

Analysis module performance: reduce allocs, CPU, and DB round-trips - #2581

Open
rh-jfuller wants to merge 3 commits into
guacsec:mainfrom
rh-jfuller:minor-perf1
Open

Analysis module performance: reduce allocs, CPU, and DB round-trips#2581
rh-jfuller wants to merge 3 commits into
guacsec:mainfrom
rh-jfuller:minor-perf1

Conversation

@rh-jfuller

@rh-jfuller rh-jfuller commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

graph analysis module had several performance inefficiencies:

  1. Per-node JSON serialization in filter() — every node scanned during a GraphQuery::Query built serde_json::Value maps for all purls and CPEs even if query only needed substring matching
  2. Deep cloning in BaseSummary — every node in the result tree cloned all purls, CPEs, and string fields from the graph into owned types
  3. Double hash lookup in Context::interncontains_key + get instead of single entry() call during graph construction
  4. Per-node HashSet<Relationship> clone — the relationship filter set was cloned for every matching node in the async closure
  5. Two seq DB queries in resolve_rh_external_sbom_descendants — first fetched the checksum value, then searched for matches
  6. No caching for resolve_external_sbom — every ExternalNode hit during descendant traversal triggered a fresh DB query, even for duplicate references

Addressing these positively impacts:

  • Memory: eliminates tens of MB transient heap allocs per query on large graphs (JSON construction), plus ~500 bytes per result node (Arc sharing vs deep clone)
  • CPU: common filter path avoids all JSON serialization
  • I/O: saves a DB round-trip per RH external node; eliminates all duplicate external resolution queries per request

No API changes. No schema changes. No migration required.

Summary by Sourcery

Improve performance of the analysis module by reducing per-node allocations and database round-trips, primarily through caching, shared ownership of summary data, and more efficient query and filtering paths.

New Features:

  • Introduce a request-scoped cache for external SBOM resolution to deduplicate database lookups across descendant traversal.

Enhancements:

  • Optimize RH external SBOM descendant resolution by replacing two sequential checksum queries with a single self-join query.
  • Avoid unnecessary JSON construction in graph filtering by only materializing nested purl and CPE structures when the query string requires them.
  • Reduce per-node memory usage in BaseSummary by sharing purl, CPE, and string fields via Arc instead of cloning.
  • Improve string interning performance during graph loading by using a single hashmap entry operation instead of separate contains/get steps.
  • Avoid cloning the relationship filter set for each node by sharing it across tasks via Arc.

Tests:

  • Update analysis service tests to reflect shared purl and CPE storage using Arc-backed slices.

…struction

- Skip JSON serialization in filter() for queries that don't use nested
  purl:/cpe: field access; the common path (full-text search, name filters)
  now uses only Value::Custom with zero JSON allocation per node
- Share graph node data in BaseSummary via Arc instead of deep cloning:
  purl/cpe use Arc<[T]>, document_id/product_name/product_version use
  Arc<String>
- Fix Context::intern double hash lookup (contains_key + get) with single
  entry() call
…de clones

Each matching node in run_graph_query cloned the HashSet<Relationship>
into the async closure. Replace with Arc::new once, Arc::clone per node.
- Collapse resolve_rh_external_sbom_descendants from two sequential
  queries into a single self-join on sbom_node_checksum, also adding
  checksum type matching
- Add ExternalSbomCache to deduplicate resolve_external_sbom calls
  during descendant traversal; uses OnceCell coalescing so concurrent
  collectors for the same external reference share one DB query
@sourcery-ai

sourcery-ai Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

This PR optimizes the analysis module by reducing per-node allocations, CPU overhead in filtering and summarization, and duplicate database lookups, primarily via smarter data sharing, conditional JSON construction, and request-scoped caching for external SBOM resolution.

Sequence diagram for cached external SBOM resolution

sequenceDiagram
    participant Collector
    participant ExternalSbomCache
    participant OnceCell
    participant Database as resolve_external_sbom

    Collector->>ExternalSbomCache: resolve(node_id, connection)
    ExternalSbomCache->>ExternalSbomCache: cache.lock()
    ExternalSbomCache->>ExternalSbomCache: cache.entry(node_id).or_default()
    ExternalSbomCache->>OnceCell: get_or_try_init(async { resolve_external_sbom(node_id, connection) })
    alt first caller for node_id
        OnceCell->>Database: resolve_external_sbom(node_id, connection)
        Database-->>OnceCell: Option<ResolvedSbom>
        OnceCell-->>ExternalSbomCache: Arc<Option<ResolvedSbom>>
    else cached result
        OnceCell-->>ExternalSbomCache: Arc<Option<ResolvedSbom>>
    end
    ExternalSbomCache-->>Collector: Option<ResolvedSbom>
    note over Collector,ExternalSbomCache: Concurrent calls for the same node_id share a single DB query via OnceCell
Loading

File-Level Changes

Change Details Files
Replace two-step checksum lookup for RH external SBOM descendants with a single self-join query and adjust downstream mapping.
  • Remove initial query that fetched checksum value for a specific node and SBOM
  • Introduce ChecksumMatch FromQueryResult struct and raw SQL self-join over sbom_node_checksum to fetch matching nodes in other SBOMs
  • Update filtering logic to use matched_node_id/matched_sbom_id fields instead of original node_id/sbom_id
  • Adjust ResolvedSbom construction to use matched_* fields
modules/analysis/src/service/mod.rs
Avoid unconditional per-node JSON construction in GraphQuery::Query filter by only building nested purl/cpe JSON when the query string actually references them.
  • Inspect query.q for "purl:" and "cpe:" markers to decide whether nested JSON is needed
  • Always expose borrowed purl and cpe collections via put_hidden for substring matching on flat fields
  • Conditionally build serde_json::Value representations for purls and cpes only when required and add them to the context
modules/analysis/src/service/mod.rs
Introduce request-scoped ExternalSbomCache to deduplicate resolve_external_sbom calls across descendants and share it across Collector instances.
  • Add ExternalSbomResult/ExternalSbomCell/ExternalSbomMap aliases and ExternalSbomCache struct backed by Arc<Mutex<...>>
  • Implement ExternalSbomCache::resolve using OnceCell::get_or_try_init to coalesce concurrent lookups per node_id
  • Thread ExternalSbomCache through Collector and AnalysisService graph collection paths
  • Use external_sbom_cache.resolve in descendant traversal instead of direct resolve_external_sbom calls
modules/analysis/src/service/collector.rs
modules/analysis/src/service/mod.rs
Reduce cloning in BaseSummary by using Arc-based shared collections/strings and a small helper for defaulting optional Arc fields.
  • Change BaseSummary.purl/cpe to Arc<[Purl]> and Arc<[Cpe]> and document_id/product_name/product_version to Arc with schema annotations
  • Define EMPTY_ARC_STRING LazyLock and arc_string_or_default helper for Option<Arc>
  • Update From<&Node> and From<&PackageNode> implementations to clone Arcs instead of deep-copying data and to use arc_string_or_default for optional string fields
  • Adjust tests and call sites to work with Arc-based fields instead of owned Vec/String values
modules/analysis/src/model.rs
modules/analysis/src/model/graph.rs
modules/analysis/src/model/roots.rs
modules/analysis/src/service/test/mod.rs
Optimize Context::intern by using HashMap::entry with or_insert_with_key instead of separate contains/get and insert operations.
  • Replace manual contains_key/get/insert sequence with a single entry(s).or_insert_with_key call that stores an Arc of the key
  • Return cloned Arc from the entry to ensure sharing while avoiding redundant lookups
modules/analysis/src/service/load/mod.rs
Minor API and test adjustments to match new shared data representations and removed imports.
  • Remove unused sbom_node_checksum import from analysis service module
  • Update tests to compare against slices dereferenced from Arc<[Purl]> and Arc<[Cpe]>
  • Use String::new() instead of "" literals for BaseSummary sbom_id/name/version/published in tests for consistency with new constructors
modules/analysis/src/service/mod.rs
modules/analysis/src/service/test/mod.rs
modules/analysis/src/model/roots.rs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've left some high level feedback:

  • The GraphQuery::Query optimization that gates nested purl/cpe materialization on query.q.contains("purl:") / "cpe:" is quite brittle; if the query syntax evolves (e.g., different casing, aliases, or structured fields), this detection will silently miss needed data, so consider delegating this decision to a parsed query structure instead of raw substring matching.
  • The new ExternalSbomCache is keyed solely by node_id; if resolve_external_sbom behavior ever depends on additional context (e.g., SBOM ID, discriminator, or external type), this cache could return incorrect cross-request results, so it would be safer to make the key reflect all inputs that affect resolution.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `GraphQuery::Query` optimization that gates nested purl/cpe materialization on `query.q.contains("purl:")` / `"cpe:"` is quite brittle; if the query syntax evolves (e.g., different casing, aliases, or structured fields), this detection will silently miss needed data, so consider delegating this decision to a parsed query structure instead of raw substring matching.
- The new `ExternalSbomCache` is keyed solely by `node_id`; if `resolve_external_sbom` behavior ever depends on additional context (e.g., SBOM ID, discriminator, or external type), this cache could return incorrect cross-request results, so it would be safer to make the key reflect all inputs that affect resolution.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@rh-jfuller
rh-jfuller requested a review from a team August 16, 2026 03:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant