Skip to content

Optimize sample and list operations, including auditing and user lookups - #8020

Open
labkey-jeckels wants to merge 13 commits into
release26.7-SNAPSHOTfrom
26.7_fb_materializedIndices
Open

Optimize sample and list operations, including auditing and user lookups#8020
labkey-jeckels wants to merge 13 commits into
release26.7-SNAPSHOTfrom
26.7_fb_materializedIndices

Conversation

@labkey-jeckels

@labkey-jeckels labkey-jeckels commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Rationale

Speed up sample and list operations by taking per-row work out of the audit write path, the list audit formatter, and import lookup resolution. Create custom sample type indices in the materialized tables so that grids over large sample types stop scanning.

Related Pull Requests

Changes

  • Collect audit events raised while building another audit record and write them in batches rather than one insert each
  • Cache each audit type's provisioned storage table instead of rebuilding a schema and table for every event inserted
  • Resolve the list audit formatter once per operation instead of once per row, and batch the row events it produces
  • Stop discarding import lookup memoization mid import so a repeated value resolves once
  • Mirror a sample type's admin defined indices onto its materialized table, and add a name index
  • Build the materialized table's non essential indices after it is published to readers so queries are not held behind the slowest index
  • Scope cached transaction audit events by container and audit log permission so they match what a database read returns
  • Add integration tests for the list audit batch boundary, the mirrored domain index, lookup memoization, and cached transaction event scoping

Tasks

  • Claude Code Review
  • Manual Testing
  • Test Automation

- Audit inserts no longer rebuild the audit table's metadata on every event
- Provider-generated audit events raised during a bulk operation are batched instead of inserted one at a time
- List inserts, updates, and deletes write their audit rows per operation rather than per row, and stop re-resolving column names for every row
- Sample timeline audit writes are chunked so a large sample operation doesn't hold every event in memory
- Fixes lookup value resolution being re-queried on every row during import, where it should have been memoized
- Audit inserts no longer rebuild the audit table's metadata on every event
- Provider-generated audit events raised during a bulk operation are batched instead of inserted one at a time
- List inserts, updates, and deletes write their audit rows per operation rather than per row, and stop re-resolving column names for every row
- Sample timeline audit writes are chunked so a large sample operation doesn't hold every event in memory
- Fixes lookup value resolution being re-queried on every row during import, where it should have been memoized
@labkey-jeckels labkey-jeckels added this to the 26.07 milestone Sep 3, 2026
@labkey-jeckels labkey-jeckels self-assigned this Sep 3, 2026
The cached provisioned TableInfo pins its Domain's DomainDescriptor, which MemTracker tracks separately, so the leak check never reached zero. StorageProvisioner.createSharedTableInfo() now owns the locking and untracking for both.
ContainerFilter cf = null == containerFilter ? ContainerFilter.current(container, user) : containerFilter;
Collection<GUID> ids = scopeIds(user, container, cf);
if (null == ids)
return event -> true;

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.

nit: Rename event to _.

if (null == ids)
return event -> true;

Set<GUID> scope = new HashSet<>(ids);

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.

Just curious, why copy the collection here? To ensure it is a set?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's more important to have a hashed collection than to ensure no dupes. Realistically, this won't be huge in practice, but better to guarantee an O(1) lookup. We should probably swap from Collection to Set in ContainerFilter but that's way out of scope.


private static String indexKey(List<String> columnNames)
{
return columnNames.stream().map(name -> name.toLowerCase(Locale.ROOT)).collect(Collectors.joining(","));

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.

Should this sort the columns so that the key is consistent/reproducible?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't think so. An index over (x,y) is not the same as one over (y,x). The UI doesn't let you define custom multi-column indices today but the API does.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants