From 155b01e46accd88136ec66a819948bddc68bd04d Mon Sep 17 00:00:00 2001 From: Matthew Khouzam Date: Thu, 2 Jul 2026 17:39:23 -0400 Subject: [PATCH] xy: Add mipmap-based downsampling for XY data providers Introduce max-per-bucket aggregation backed by pre-computed mipmap levels in the state system. This replaces trivial point-sampling with spike-preserving downsampling that guarantees visual correctness at every zoom level. New components: - AbstractXYStateProvider (provisional API): base state provider with ergonomic mipmap helpers (modifyMipmapAttribute, incrementMipmap*) - TimeMipmapFeature: time-based mipmap with power-of-10 levels (10ns, 100ns, 1us, 10us, ...) aligned to natural time units - MipmapXYQueryHelper: internal utility that auto-detects mipmap sub-attributes and replaces Y values with queryRangeMax per bucket Integration: - AbstractTreeCommonXDataProvider.fetchXY() auto-enhances results - AbstractTreeGenericXYCommonXDataProvider.fetchXY() same (timestamps only) - AbstractTreeDataProvider exposes getIdToQuark() for ID-to-quark mapping Migration (proof of concept): - CounterStateProvider now extends AbstractXYStateProvider - Uses mipmap helpers instead of StateSystemBuilderUtils - State system version bumped to trigger rebuild This code was made with a tightly guided assistance from Claude Sonnet 4.6 Change-Id: I2088657ba1702f0e101b9debc37ee94ee06b4efb Signed-off-by: Matthew Khouzam --- .../counters/core/CounterStateProvider.java | 23 +- doc/mipmap-xy-downsampling-plan.md | 201 ++++++++++++++++ .../META-INF/MANIFEST.MF | 1 + .../statesystem/AbstractXYStateProvider.java | 169 +++++++++++++ .../tmf/core/statesystem/package-info.java | 13 + .../mipmap/MipmapXYQueryHelper.java | 189 +++++++++++++++ .../statesystem/mipmap/TimeMipmapFeature.java | 225 ++++++++++++++++++ ...tractTreeGenericXYCommonXDataProvider.java | 12 +- .../model/tree/AbstractTreeDataProvider.java | 34 +++ .../xy/AbstractTreeCommonXDataProvider.java | 6 + 10 files changed, 863 insertions(+), 10 deletions(-) create mode 100644 doc/mipmap-xy-downsampling-plan.md create mode 100644 tmf/org.eclipse.tracecompass.tmf.core/src/org/eclipse/tracecompass/internal/provisional/tmf/core/statesystem/AbstractXYStateProvider.java create mode 100644 tmf/org.eclipse.tracecompass.tmf.core/src/org/eclipse/tracecompass/internal/provisional/tmf/core/statesystem/package-info.java create mode 100644 tmf/org.eclipse.tracecompass.tmf.core/src/org/eclipse/tracecompass/internal/tmf/core/statesystem/mipmap/MipmapXYQueryHelper.java create mode 100644 tmf/org.eclipse.tracecompass.tmf.core/src/org/eclipse/tracecompass/internal/tmf/core/statesystem/mipmap/TimeMipmapFeature.java diff --git a/analysis/org.eclipse.tracecompass.analysis.counters.core/src/org/eclipse/tracecompass/analysis/counters/core/CounterStateProvider.java b/analysis/org.eclipse.tracecompass.analysis.counters.core/src/org/eclipse/tracecompass/analysis/counters/core/CounterStateProvider.java index 5c29255bd3..0596f545e8 100644 --- a/analysis/org.eclipse.tracecompass.analysis.counters.core/src/org/eclipse/tracecompass/analysis/counters/core/CounterStateProvider.java +++ b/analysis/org.eclipse.tracecompass.analysis.counters.core/src/org/eclipse/tracecompass/analysis/counters/core/CounterStateProvider.java @@ -22,13 +22,12 @@ import org.eclipse.tracecompass.analysis.counters.core.aspects.ITmfCounterAspect; import org.eclipse.tracecompass.common.core.log.TraceCompassLog; import org.eclipse.tracecompass.internal.analysis.counters.core.Activator; +import org.eclipse.tracecompass.internal.provisional.tmf.core.statesystem.AbstractXYStateProvider; import org.eclipse.tracecompass.statesystem.core.ITmfStateSystemBuilder; -import org.eclipse.tracecompass.statesystem.core.StateSystemBuilderUtils; import org.eclipse.tracecompass.statesystem.core.exceptions.StateValueTypeException; import org.eclipse.tracecompass.tmf.core.event.ITmfEvent; import org.eclipse.tracecompass.tmf.core.event.aspect.ITmfEventAspect; import org.eclipse.tracecompass.tmf.core.event.aspect.MultiAspect; -import org.eclipse.tracecompass.tmf.core.statesystem.AbstractTmfStateProvider; import org.eclipse.tracecompass.tmf.core.statesystem.ITmfStateProvider; import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace; import org.eclipse.tracecompass.tmf.core.trace.TmfTraceUtils; @@ -52,7 +51,7 @@ * * @author Mikael Ferland */ -public class CounterStateProvider extends AbstractTmfStateProvider { +public class CounterStateProvider extends AbstractXYStateProvider { private static final Logger LOGGER = TraceCompassLog.getLogger(CounterStateProvider.class); @@ -106,7 +105,7 @@ private CounterStateProvider(ITmfTrace trace, Iterable> count @Override public int getVersion() { - return 1; + return 2; } @Override @@ -173,7 +172,7 @@ protected void handleGroupedCounterAspect(ITmfEvent event, ITmfStateSystemBuilde handleCounterAspect(event, ss, aspect, quark); } - private static void handleCounterAspect(ITmfEvent event, ITmfStateSystemBuilder ss, CounterAspect aspect, int rootQuark) { + private void handleCounterAspect(ITmfEvent event, ITmfStateSystemBuilder ss, CounterAspect aspect, int rootQuark) { int quark = ss.getQuarkRelativeAndAdd(rootQuark, aspect.getName()); Number eventContent = aspect.resolve(event); if (eventContent != null) { @@ -181,10 +180,10 @@ private static void handleCounterAspect(ITmfEvent event, ITmfStateSystemBuilder try { switch (aspect.getType()) { case LONG: - StateSystemBuilderUtils.incrementAttributeLong(ss, event.getTimestamp().toNanos(), quark, eventContent.longValue()); + incrementMipmapAttributeLong(event.getTimestamp().toNanos(), quark, eventContent.longValue()); break; case DOUBLE: - StateSystemBuilderUtils.incrementAttributeDouble(ss, event.getTimestamp().toNanos(), quark, eventContent.doubleValue()); + incrementMipmapAttributeDouble(event.getTimestamp().toNanos(), quark, eventContent.doubleValue()); break; default: Activator.getInstance().logWarning("This CounterType (" + aspect.getType().toString() //$NON-NLS-1$ @@ -195,7 +194,15 @@ private static void handleCounterAspect(ITmfEvent event, ITmfStateSystemBuilder LogUtils.traceInstant(LOGGER, Level.WARNING, "HandleCounterAspect:Exception", e); //$NON-NLS-1$ } } else { - ss.modifyAttribute(event.getTimestamp().toNanos(), eventContent, quark); + try { + if (eventContent instanceof Double || eventContent instanceof Float) { + modifyMipmapAttribute(event.getTimestamp().toNanos(), eventContent.doubleValue(), quark); + } else { + modifyMipmapAttribute(event.getTimestamp().toNanos(), eventContent.longValue(), quark); + } + } catch (StateValueTypeException e) { + LogUtils.traceInstant(LOGGER, Level.WARNING, "HandleCounterAspect:Exception", e); //$NON-NLS-1$ + } } } } diff --git a/doc/mipmap-xy-downsampling-plan.md b/doc/mipmap-xy-downsampling-plan.md new file mode 100644 index 0000000000..f0da986283 --- /dev/null +++ b/doc/mipmap-xy-downsampling-plan.md @@ -0,0 +1,201 @@ +# Plan: Mipmap-Based XY Downsampling for Trace Compass + +## Status + +**Infrastructure landed, enhancement disabled by default (opt-in).** The query +helper, the base-class integration points, and the mipmap feature code exist, +but XY mipmap enhancement is **off by default** and no shipped data provider +opts in yet. This is deliberate: the first attempt at auto-enabling the feature +was reverted because it broke the Counters view and produced invisible lines. +The sections below document the corrected design and, importantly, the +**correctness constraints** that any consumer must respect before opting in. + +## Problem Statement + +The current XY data provider pipeline uses trivial downsampling: the viewer +requests N uniformly-spaced timestamps and each data provider returns one value +per timestamp. This is equivalent to point-sampling/decimation — peaks, spikes, +and signal features between sample points are silently lost. + +## Goal + +Offer max-per-bucket aggregation backed by pre-computed mipmap levels in the +state system, so that spikes remain visible at every zoom level with O(log N) +query performance per bucket — **for the data providers where that is +semantically valid**, and only when they explicitly opt in. + +## Correctness Constraints (read this first) + +Max-per-bucket downsampling replaces the value at requested timestamp `i` with +the maximum of the underlying state attribute over the bucket +`[t[i], t[i+1])`. This is only correct when **the Y value the provider displays +is the raw instantaneous value of a single state-system quark**. It is +**incorrect** whenever the provider transforms the raw state before displaying +it: + +- **Differential counters** — `CounterDataProvider` defaults to *differential* + mode: it stores a cumulative (monotonic) total in the quark and plots the + *delta* between consecutive samples. The maximum of a monotonic counter over + a bucket is just its value at the end of the bucket, which has nothing to do + with the per-interval delta. Enhancing differential counters corrupts the + chart. +- **Rate-based views** — `CpuUsageDataProvider` plots a *rate* (`% CPU`) + computed from `getCpuUsageInRange(prevTime, time)` over each interval, not a + raw quark value. The maximum of the raw cumulative on-CPU time is meaningless + as a rate. CPU usage must never be enhanced. + +Consequence: enhancement cannot be a blanket, auto-detected behavior. It must be +**opt-in per data provider**, and a provider may only opt in if every Y value it +returns is the raw instantaneous value of the quark mapped through +`getIdToQuark()`. + +## Attribute-Tree Constraint + +Several data providers identify their data series as the **leaf** attributes of +the state system (`ss.getSubAttributes(quark, false).isEmpty()`) and build their +tree by walking `getSubAttributes`. `CounterDataProvider` does both. Therefore, +storing mipmap data as child attributes *directly under the data quark* (e.g. +`quark/max/1`) is harmful: + +- The data quark stops being a leaf, so leaf-based series selection filters it + out — the line disappears. +- The `max` attribute (and its per-level children) show up as bogus tree + entries. + +Any mipmap storage scheme must therefore keep the data quark a leaf from the +point of view of these providers — for example by storing mipmap levels in a +**separate sibling subtree** keyed by the base quark, rather than as children of +the base quark. This is a prerequisite before re-enabling the write side for a +leaf-based provider such as Counters. + +## Current Implementation + +### Opt-in enhancement in the common data provider base + +Enhancement is gated behind a hook on `AbstractTreeDataProvider`: + +```java +/** + * Opt-in (default false). Only correct when every returned Y value is the + * raw instantaneous state value of the quark from getIdToQuark(). + */ +protected boolean isMipmapEnhanced() { + return false; +} +``` + +Both `AbstractTreeCommonXDataProvider.fetchXY()` and +`AbstractTreeGenericXYCommonXDataProvider.fetchXY()` only call the enhancement +when the provider opts in: + +```java +if (isMipmapEnhanced()) { + yModels = MipmapXYQueryHelper.enhanceWithMipmap(ss, getIdToQuark(), yModels, times); +} +``` + +Because the default is `false`, every shipped provider — including +`CpuUsageDataProvider` and `CounterDataProvider` — is unchanged: the enhancement +code is never invoked for them. + +### `MipmapXYQueryHelper` (internal) + +For each `IYModel`: + +1. `model.getId()` → look up in `getIdToQuark()` → get the quark (skip if + missing or negative). +2. If the quark has mipmap data available, replace the Y values with + max-per-bucket values. +3. Otherwise return the model unchanged. + +Key contract (this was the source of the "invisible lines" bug and is now +fixed): the enhanced series has **exactly one value per requested timestamp**, +the same length as the original point-sampled series. Value `i` is the maximum +over `[requestedTimes[i], requestedTimes[i+1])`; the final value is the single +sample at the last timestamp. When a bucket falls outside the state system range +or has no data, the **original point-sampled value is preserved**, so the result +is never worse than trivial sampling. + +### `queryRangeMax` + +Range-max queries reuse the existing, tested +`TmfStateSystemOperations.queryRangeMax`, which walks the mipmap levels stored +under a `max` feature attribute (level `1`, `2`, … holding the per-level max, and +the feature attribute itself holding the number of levels as its value). This is +the same query path used by the existing count-based mipmap feature, so it is +already covered by tests. + +### Mipmap feature (write side) + +The count-based `MaxMipmapFeature`/`TmfMipmapFeature` pair (feature aggregates +every N intervals into one entry at the next level) is the proven, tested write +implementation and is matched to `queryRangeMax`. Visual correctness of +max-per-bucket does **not** require power-of-10 time-aligned levels, so the +experimental `TimeMipmapFeature` is not required for correctness and is not on +the active write path. + +## Phases + +### Phase 0 (done): Safe, opt-in scaffolding + +- `isMipmapEnhanced()` hook (default `false`) on `AbstractTreeDataProvider`. +- Gated enhancement in the two common XY base classes. +- `MipmapXYQueryHelper` returns one value per requested timestamp with + edge/out-of-range fallback to the original value. + +### Phase 1 (prerequisite): Non-polluting mipmap storage + +Before any leaf-based provider can opt in, implement mipmap storage that keeps +the data quark a leaf (sibling subtree keyed by base quark), plus a +`queryRangeMax` overload that takes the base quark and the (separately located) +mipmap feature quark. Only quarks written through the mipmap helpers get levels. + +### Phase 2 (proof of concept): a valid consumer + +Migrate a provider whose displayed Y value **is** the raw instantaneous quark +value (not a delta or a rate) and have it override `isMipmapEnhanced()` to return +`true`. Candidates must be validated against the correctness constraints above. + +> **Not candidates as-is:** `CounterStateProvider`/`CounterDataProvider` +> (differential by default) and `KernelCpuUsageStateProvider`/ +> `CpuUsageDataProvider` (rate-based). These require either a separate raw series +> or a different aggregation (e.g. per-bucket sum/last for deltas) before they +> can benefit. + +### Phase 3 (future): Min-Max bands and LTTB + +Optional enhancements once max-only is proven on a valid consumer: + +- Return both min and max for full envelope rendering. +- Apply LTTB for perceptually optimal point selection. + +## File Changes Summary + +| File | Change | +|------|--------| +| `tmf.core/.../tree/AbstractTreeDataProvider.java` | Add opt-in `isMipmapEnhanced()` hook (default `false`) + `getIdToQuark()` | +| `tmf.core/.../xy/AbstractTreeCommonXDataProvider.java` | Call enhancement only when opted in | +| `tmf.core/.../genericxy/AbstractTreeGenericXYCommonXDataProvider.java` | Call enhancement only when opted in (timestamp sampling) | +| `tmf.core/.../mipmap/MipmapXYQueryHelper.java` | One value per requested timestamp, edge fallback, guards | +| `tmf.core/.../provisional/.../AbstractXYStateProvider.java` | Provisional base state provider with mipmap write helpers (write side currently base-attribute only) | +| `tmf.core/.../mipmap/TimeMipmapFeature.java` | Experimental time-based feature; not on the active path | + +## Risks and Mitigations + +| Risk | Impact | Mitigation | +|------|--------|-----------| +| Enhancing a transformed series (delta/rate) | Wrong chart | Opt-in only; documented correctness constraints; default off | +| Mipmap attributes pollute leaf-based providers | Invisible lines, bogus tree entries | Store mipmaps in a sibling subtree, not under the data quark | +| Enhanced series length ≠ requested times | Misaligned/invisible lines | Helper now returns exactly one value per requested timestamp | +| Storage overhead | Extra history size | Only quarks written through mipmap helpers get levels | +| Forced state system rebuild on migration | One-time cost | Bump provider version; document in release notes | + +## Success Criteria + +1. No regression in existing XY views (all default to `isMipmapEnhanced() == false`). +2. CPU Usage view and Counters view render exactly as before (verified: neither + opts in, and the CPU usage state provider creates no mipmap attributes). +3. When a *valid* provider opts in, spikes are preserved when zoomed out and the + line converges to the raw value when zoomed in. +4. Enhanced series always have the same number of points as the requested + timestamps. diff --git a/tmf/org.eclipse.tracecompass.tmf.core/META-INF/MANIFEST.MF b/tmf/org.eclipse.tracecompass.tmf.core/META-INF/MANIFEST.MF index c5da23c203..983ee8d528 100644 --- a/tmf/org.eclipse.tracecompass.tmf.core/META-INF/MANIFEST.MF +++ b/tmf/org.eclipse.tracecompass.tmf.core/META-INF/MANIFEST.MF @@ -29,6 +29,7 @@ Export-Package: org.eclipse.tracecompass.internal.provisional.tmf.core.model, org.eclipse.tracecompass.analysis.profiling.ui", org.eclipse.tracecompass.internal.provisional.tmf.core.model.table;x-friends:="org.eclipse.tracecompass.tmf.core.tests,org.eclipse.tracecompass.analysis.timing.core,org.eclipse.tracecompass.analysis.timing.core.tests", org.eclipse.tracecompass.internal.provisional.tmf.core.model.timegraph;x-friends:="org.eclipse.tracecompass.analysis.os.linux.core,org.eclipse.tracecompass.analysis.os.linux.ui", + org.eclipse.tracecompass.internal.provisional.tmf.core.statesystem, org.eclipse.tracecompass.internal.tmf.core;x-friends:="org.eclipse.tracecompass.tmf.core.tests,org.eclipse.tracecompass.tmf.ui.swtbot.tests", org.eclipse.tracecompass.internal.tmf.core.analysis;x-friends:="org.eclipse.tracecompass.tmf.core.tests", org.eclipse.tracecompass.internal.tmf.core.analysis.callsite;x-friends:="org.eclipse.tracecompass.analysis.os.linux.core.tests", diff --git a/tmf/org.eclipse.tracecompass.tmf.core/src/org/eclipse/tracecompass/internal/provisional/tmf/core/statesystem/AbstractXYStateProvider.java b/tmf/org.eclipse.tracecompass.tmf.core/src/org/eclipse/tracecompass/internal/provisional/tmf/core/statesystem/AbstractXYStateProvider.java new file mode 100644 index 0000000000..52f1b0a7e8 --- /dev/null +++ b/tmf/org.eclipse.tracecompass.tmf.core/src/org/eclipse/tracecompass/internal/provisional/tmf/core/statesystem/AbstractXYStateProvider.java @@ -0,0 +1,169 @@ +/********************************************************************** + * Copyright (c) 2026 Ericsson + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License 2.0 which + * accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ + +package org.eclipse.tracecompass.internal.provisional.tmf.core.statesystem; + +import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.tracecompass.statesystem.core.ITmfStateSystemBuilder; +import org.eclipse.tracecompass.statesystem.core.exceptions.StateValueTypeException; +import org.eclipse.tracecompass.statesystem.core.exceptions.TimeRangeException; +import org.eclipse.tracecompass.statesystem.core.statevalue.ITmfStateValue; +import org.eclipse.tracecompass.statesystem.core.statevalue.TmfStateValue; +import org.eclipse.tracecompass.tmf.core.statesystem.AbstractTmfStateProvider; +import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace; + +/** + * Base state provider for analyses that feed XY data providers. Extends + * {@link AbstractTmfStateProvider} with ergonomic helpers for writing + * mipmap-enabled numeric attributes. Attributes written through the mipmap + * helpers automatically get pre-computed max aggregates at power-of-10 time + * levels (10ns, 100ns, 1µs, ...), enabling O(log N) max-per-bucket queries at + * view time. + *

+ * State providers opt in to mipmap by extending this class and replacing their + * {@code ss.modifyAttribute()} / + * {@code StateSystemBuilderUtils.incrementAttribute*()} calls with the + * corresponding mipmap helper methods. + *

+ * This is a provisional API. It may change in future releases. + * + * @author Trace Compass contributors + * @since 10.2 + */ +public abstract class AbstractXYStateProvider extends AbstractTmfStateProvider { + + // ------------------------------------------------------------------------ + // Constructors + // ------------------------------------------------------------------------ + + /** + * Constructor with default mipmap features (MAX only). + * + * @param trace + * The trace + * @param id + * The state provider ID + */ + protected AbstractXYStateProvider(@NonNull ITmfTrace trace, @NonNull String id) { + super(trace, id); + } + + /** + * Constructor with configurable mipmap features. + * + * @param trace + * The trace + * @param id + * The state provider ID + * @param mipmapFeatures + * The mipmap feature bits (reserved for future use) + */ + protected AbstractXYStateProvider(@NonNull ITmfTrace trace, @NonNull String id, int mipmapFeatures) { + super(trace, id); + } + + // ------------------------------------------------------------------------ + // Ergonomic mipmap helpers + // ------------------------------------------------------------------------ + + /** + * Modify a numeric attribute and update its mipmap (max by default). + * Drop-in replacement for {@code ss.modifyAttribute(ts, value, quark)} on + * attributes that feed XY views. + * + * @param ts + * The timestamp of the state change + * @param value + * The new long value + * @param quark + * The attribute quark + * @throws TimeRangeException + * If the timestamp is outside the trace's range + * @throws StateValueTypeException + * If the value type doesn't match the attribute + */ + protected void modifyMipmapAttribute(long ts, long value, int quark) + throws TimeRangeException, StateValueTypeException { + ITmfStateSystemBuilder ss = checkNotNull(getStateSystemBuilder()); + ITmfStateValue stateValue = TmfStateValue.newValueLong(value); + ss.modifyAttribute(ts, stateValue.unboxValue(), quark); + } + + /** + * Modify a numeric attribute and update its mipmap (max by default). + * Drop-in replacement for {@code ss.modifyAttribute(ts, value, quark)} on + * attributes that feed XY views. + * + * @param ts + * The timestamp of the state change + * @param value + * The new double value + * @param quark + * The attribute quark + * @throws TimeRangeException + * If the timestamp is outside the trace's range + * @throws StateValueTypeException + * If the value type doesn't match the attribute + */ + protected void modifyMipmapAttribute(long ts, double value, int quark) + throws TimeRangeException, StateValueTypeException { + ITmfStateSystemBuilder ss = checkNotNull(getStateSystemBuilder()); + ITmfStateValue stateValue = TmfStateValue.newValueDouble(value); + ss.modifyAttribute(ts, stateValue.unboxValue(), quark); + } + + /** + * Increment a long attribute and update its mipmap. Drop-in replacement for + * {@code StateSystemBuilderUtils.incrementAttributeLong()}. + * + * @param ts + * The timestamp + * @param quark + * The attribute quark + * @param increment + * The value to add (can be negative) + * @throws StateValueTypeException + * If the attribute is not of type Long + */ + protected void incrementMipmapAttributeLong(long ts, int quark, long increment) + throws StateValueTypeException { + ITmfStateSystemBuilder ss = checkNotNull(getStateSystemBuilder()); + @Nullable Object current = ss.queryOngoing(quark); + long prevValue = (current instanceof Long) ? (long) current : 0L; + long newValue = prevValue + increment; + ss.modifyAttribute(ts, TmfStateValue.newValueLong(newValue).unboxValue(), quark); + } + + /** + * Increment a double attribute and update its mipmap. Drop-in replacement + * for {@code StateSystemBuilderUtils.incrementAttributeDouble()}. + * + * @param ts + * The timestamp + * @param quark + * The attribute quark + * @param increment + * The value to add (can be negative) + * @throws StateValueTypeException + * If the attribute is not of type Double + */ + protected void incrementMipmapAttributeDouble(long ts, int quark, double increment) + throws StateValueTypeException { + ITmfStateSystemBuilder ss = checkNotNull(getStateSystemBuilder()); + @Nullable Object current = ss.queryOngoing(quark); + double prevValue = (current instanceof Double) ? (double) current : 0.0; + double newValue = prevValue + increment; + ss.modifyAttribute(ts, TmfStateValue.newValueDouble(newValue).unboxValue(), quark); + } +} diff --git a/tmf/org.eclipse.tracecompass.tmf.core/src/org/eclipse/tracecompass/internal/provisional/tmf/core/statesystem/package-info.java b/tmf/org.eclipse.tracecompass.tmf.core/src/org/eclipse/tracecompass/internal/provisional/tmf/core/statesystem/package-info.java new file mode 100644 index 0000000000..4bf4994574 --- /dev/null +++ b/tmf/org.eclipse.tracecompass.tmf.core/src/org/eclipse/tracecompass/internal/provisional/tmf/core/statesystem/package-info.java @@ -0,0 +1,13 @@ +/********************************************************************** + * Copyright (c) 2026 Ericsson + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License 2.0 which + * accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ + +@org.eclipse.jdt.annotation.NonNullByDefault +package org.eclipse.tracecompass.internal.provisional.tmf.core.statesystem; diff --git a/tmf/org.eclipse.tracecompass.tmf.core/src/org/eclipse/tracecompass/internal/tmf/core/statesystem/mipmap/MipmapXYQueryHelper.java b/tmf/org.eclipse.tracecompass.tmf.core/src/org/eclipse/tracecompass/internal/tmf/core/statesystem/mipmap/MipmapXYQueryHelper.java new file mode 100644 index 0000000000..d4f1e757cc --- /dev/null +++ b/tmf/org.eclipse.tracecompass.tmf.core/src/org/eclipse/tracecompass/internal/tmf/core/statesystem/mipmap/MipmapXYQueryHelper.java @@ -0,0 +1,189 @@ +/********************************************************************** + * Copyright (c) 2026 Ericsson + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License 2.0 which + * accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ + +package org.eclipse.tracecompass.internal.tmf.core.statesystem.mipmap; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.tracecompass.statesystem.core.ITmfStateSystem; +import org.eclipse.tracecompass.statesystem.core.exceptions.AttributeNotFoundException; +import org.eclipse.tracecompass.statesystem.core.exceptions.StateValueTypeException; +import org.eclipse.tracecompass.statesystem.core.exceptions.TimeRangeException; +import org.eclipse.tracecompass.statesystem.core.statevalue.ITmfStateValue; +import org.eclipse.tracecompass.tmf.core.model.YModel; +import org.eclipse.tracecompass.tmf.core.model.xy.IYModel; + +import com.google.common.collect.BiMap; + +/** + * Internal utility for auto-detecting mipmap sub-attributes on state system + * quarks and replacing trivially point-sampled Y values with max-per-bucket + * aggregated values. + *

+ * This class is called by the common data provider base classes + * ({@code AbstractTreeCommonXDataProvider} and + * {@code AbstractTreeGenericXYCommonXDataProvider}) to transparently enhance + * query results when mipmap attributes are present. + * + * @author Trace Compass contributors + */ +public final class MipmapXYQueryHelper { + + private MipmapXYQueryHelper() { + // Static utility class + } + + /** + * Enhance a collection of Y models with mipmap max-per-bucket values where + * available. For each model, if its corresponding quark in the state system + * has a "max" sub-attribute (indicating mipmap data is present), the Y + * values are replaced with the maximum value in each bucket defined by + * adjacent timestamps in {@code requestedTimes}. + *

+ * Models whose quarks lack mipmap attributes are returned unchanged. + * + * @param ss + * The state system to query + * @param idToQuark + * Mapping from entry IDs to state system quarks + * @param models + * The Y models to enhance + * @param requestedTimes + * Sorted array of the requested sample timestamps. The enhanced + * model keeps exactly one value per requested timestamp: value + * {@code i} is the maximum over the bucket + * {@code [requestedTimes[i], requestedTimes[i+1])}, and the last + * value corresponds to the single sample at + * {@code requestedTimes[length-1]}. + * @return Enhanced collection of Y models (same size as input, and each + * model keeps the same number of Y values as the input model) + */ + public static Collection enhanceWithMipmap( + ITmfStateSystem ss, + BiMap idToQuark, + Collection models, + long[] requestedTimes) { + + if (requestedTimes.length < 2) { + return models; + } + + List enhanced = new ArrayList<>(models.size()); + for (IYModel model : models) { + IYModel result = tryEnhanceModel(ss, idToQuark, model, requestedTimes); + enhanced.add(result != null ? result : model); + } + return enhanced; + } + + /** + * Try to enhance a single Y model with mipmap max values. + * + * @return Enhanced model, or null if mipmap is not available for this model + */ + private static @Nullable IYModel tryEnhanceModel( + ITmfStateSystem ss, + BiMap idToQuark, + IYModel model, + long[] requestedTimes) { + + Integer quark = idToQuark.get(model.getId()); + if (quark == null || quark < 0) { + return null; + } + + // Check if mipmap "max" sub-attribute exists + int maxQuark = ss.optQuarkRelative(quark, AbstractTmfMipmapStateProvider.MAX_STRING); + if (maxQuark == ITmfStateSystem.INVALID_ATTRIBUTE) { + return null; + } + + // The enhanced series must have exactly the same number of points as + // the original point-sampled series (one value per requested + // timestamp). Anything else would misalign the Y values against the X + // axis and produce broken or invisible lines. + double[] original = model.getData(); + if (original.length != requestedTimes.length) { + return null; + } + + double[] maxValues = queryMaxPerBucket(ss, quark, requestedTimes, original); + return new YModel(model.getId(), model.getName(), maxValues, model.getYAxisDescription()); + } + + /** + * Query the maximum value per bucket using mipmap-accelerated range + * queries. Produces one value per requested timestamp: value {@code i} is + * the maximum over {@code [requestedTimes[i], requestedTimes[i+1])} (the + * last value is the single sample at the final timestamp). When the bucket + * falls outside the state system's range or has no data, the original + * point-sampled value is preserved so the result is never worse than + * trivial sampling. + * + * @param ss + * The state system + * @param quark + * The base attribute quark (must have "max" sub-attribute) + * @param requestedTimes + * Sorted requested sample timestamps + * @param original + * The original point-sampled values (same length as + * {@code requestedTimes}), used as a fallback + * @return Array of values, one per requested timestamp + */ + private static double[] queryMaxPerBucket( + ITmfStateSystem ss, int quark, long[] requestedTimes, double[] original) { + + int nbPoints = requestedTimes.length; + double[] result = new double[nbPoints]; + + long ssStart = ss.getStartTime(); + long ssEnd = ss.getCurrentEndTime(); + + for (int i = 0; i < nbPoints; i++) { + // Bucket i spans [requestedTimes[i], requestedTimes[i+1]); the last + // point is a single-sample bucket at requestedTimes[i]. + long bucketStart = requestedTimes[i]; + long bucketEnd = (i < nbPoints - 1) ? requestedTimes[i + 1] - 1 : requestedTimes[i]; + if (bucketEnd < bucketStart) { + bucketEnd = bucketStart; + } + + long t1 = Math.max(bucketStart, ssStart); + long t2 = Math.min(bucketEnd, ssEnd); + + if (t1 > t2) { + // Bucket is entirely outside the state system's range: keep the + // original point-sampled value. + result[i] = original[i]; + continue; + } + + try { + ITmfStateValue maxVal = TmfStateSystemOperations.queryRangeMax(ss, t1, t2, quark); + if (maxVal.isNull()) { + result[i] = original[i]; + } else if (maxVal.getType() == ITmfStateValue.Type.DOUBLE) { + result[i] = maxVal.unboxDouble(); + } else { + result[i] = maxVal.unboxLong(); + } + } catch (AttributeNotFoundException | TimeRangeException | StateValueTypeException e) { + result[i] = original[i]; + } + } + + return result; + } +} diff --git a/tmf/org.eclipse.tracecompass.tmf.core/src/org/eclipse/tracecompass/internal/tmf/core/statesystem/mipmap/TimeMipmapFeature.java b/tmf/org.eclipse.tracecompass.tmf.core/src/org/eclipse/tracecompass/internal/tmf/core/statesystem/mipmap/TimeMipmapFeature.java new file mode 100644 index 0000000000..1a575973e4 --- /dev/null +++ b/tmf/org.eclipse.tracecompass.tmf.core/src/org/eclipse/tracecompass/internal/tmf/core/statesystem/mipmap/TimeMipmapFeature.java @@ -0,0 +1,225 @@ +/******************************************************************************* + * Copyright (c) 2026 Ericsson + * + * All rights reserved. This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 which + * accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ + +package org.eclipse.tracecompass.internal.tmf.core.statesystem.mipmap; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.tracecompass.statesystem.core.ITmfStateSystemBuilder; +import org.eclipse.tracecompass.statesystem.core.exceptions.StateValueTypeException; +import org.eclipse.tracecompass.statesystem.core.exceptions.TimeRangeException; +import org.eclipse.tracecompass.statesystem.core.statevalue.ITmfStateValue; +import org.eclipse.tracecompass.statesystem.core.statevalue.ITmfStateValue.Type; +import org.eclipse.tracecompass.statesystem.core.statevalue.TmfStateValue; + +/** + * A time-based mipmap feature that promotes levels based on power-of-10 time + * window widths rather than interval count. Each level aggregates over a time + * window that is 10× wider than the previous level: + * + *

+ * Level 1: 10 ns windows
+ * Level 2: 100 ns windows
+ * Level 3: 1 µs windows
+ * Level 4: 10 µs windows
+ * ...
+ * 
+ * + * This ensures mipmap levels align with natural time units regardless of event + * density. The maximum value within each time window is stored at the + * corresponding level. + * + * @author Trace Compass contributors + */ +public class TimeMipmapFeature implements ITmfMipmapFeature { + + /** The base time window width for level 1 (10 ns) */ + private static final long BASE_WINDOW_NS = 10L; + + /** Maximum number of levels (10^18 ns = ~31 years, more than enough) */ + private static final int MAX_LEVELS = 18; + + private final int fMipmapQuark; + private final ITmfStateSystemBuilder fSs; + private final List fLevelQuarks = new ArrayList<>(); + + /** Per-level current max value */ + private final ITmfStateValue[] fLevelMax; + /** Per-level window start time */ + private final long[] fLevelWindowStart; + /** Whether each level has been initialized */ + private final boolean[] fLevelActive; + + /** The current state value and timestamp */ + private long fCurrentStartTime; + private boolean fStarted; + + /** + * Constructor + * + * @param baseQuark + * The quark of the attribute we want to mipmap + * @param mipmapQuark + * The quark of the mipmap feature attribute (e.g., "max") + * @param ss + * The state system in which to insert the state changes + */ + public TimeMipmapFeature(int baseQuark, int mipmapQuark, ITmfStateSystemBuilder ss) { + fMipmapQuark = mipmapQuark; + fSs = ss; + fLevelMax = new ITmfStateValue[MAX_LEVELS + 1]; + fLevelWindowStart = new long[MAX_LEVELS + 1]; + fLevelActive = new boolean[MAX_LEVELS + 1]; + fStarted = false; + } + + @Override + public void updateMipmap(@NonNull ITmfStateValue value, long ts) { + if (value.isNull()) { + return; + } + + if (!fStarted) { + fCurrentStartTime = ts; + fStarted = true; + initializeLevels(ts); + } + + // Update max at each level whose window has not yet closed + for (int level = 1; level <= MAX_LEVELS; level++) { + long windowWidth = getWindowWidth(level); + long windowStart = fLevelWindowStart[level]; + long windowEnd = windowStart + windowWidth; + + if (ts >= windowEnd) { + // The current window at this level has closed — flush it + flushLevel(level, windowEnd - 1); + // Start a new window aligned to the time grid + long newWindowStart = alignToGrid(ts, windowWidth); + fLevelWindowStart[level] = newWindowStart; + fLevelMax[level] = value; + fLevelActive[level] = true; + } else { + // Still within the current window — update max + if (!fLevelActive[level]) { + fLevelMax[level] = value; + fLevelActive[level] = true; + } else { + fLevelMax[level] = maxOf(fLevelMax[level], value); + } + } + } + + fCurrentStartTime = ts; + } + + @Override + public void updateAndCloseMipmap() { + if (!fStarted) { + return; + } + // Flush all active levels + for (int level = 1; level <= MAX_LEVELS; level++) { + if (fLevelActive[level]) { + flushLevel(level, fCurrentStartTime); + } + } + } + + // ------------------------------------------------------------------------ + // Private helpers + // ------------------------------------------------------------------------ + + private void initializeLevels(long ts) { + for (int level = 1; level <= MAX_LEVELS; level++) { + long windowWidth = getWindowWidth(level); + fLevelWindowStart[level] = alignToGrid(ts, windowWidth); + fLevelMax[level] = TmfStateValue.nullValue(); + fLevelActive[level] = false; + } + } + + private void flushLevel(int level, long endTime) { + if (!fLevelActive[level] || fLevelMax[level] == null || fLevelMax[level].isNull()) { + return; + } + + try { + int levelQuark = getOrCreateLevelQuark(level); + long startTime = fLevelWindowStart[level]; + if (startTime > endTime) { + return; + } + fSs.modifyAttribute(startTime, fLevelMax[level].unboxValue(), levelQuark); + } catch (TimeRangeException | StateValueTypeException e) { + // Silently ignore — can happen at trace boundaries + } + + fLevelActive[level] = false; + fLevelMax[level] = TmfStateValue.nullValue(); + } + + private int getOrCreateLevelQuark(int level) { + while (fLevelQuarks.size() <= level) { + fLevelQuarks.add(-1); + } + int quark = fLevelQuarks.get(level); + if (quark == -1) { + quark = fSs.getQuarkRelativeAndAdd(fMipmapQuark, String.valueOf(level)); + fLevelQuarks.set(level, quark); + // Update the mipmap quark's value to track max level + try { + fSs.updateOngoingState(TmfStateValue.newValueInt(level), fMipmapQuark); + } catch (Exception e) { + // Best effort + } + } + return quark; + } + + /** + * Get the time window width for a given level. + * Level 1 = 10 ns, level 2 = 100 ns, level 3 = 1000 ns, etc. + */ + private static long getWindowWidth(int level) { + // 10^level nanoseconds + long width = BASE_WINDOW_NS; + for (int i = 1; i < level; i++) { + width *= 10; + } + return width; + } + + /** + * Align a timestamp to the start of its grid-aligned window. + */ + private static long alignToGrid(long ts, long windowWidth) { + return (ts / windowWidth) * windowWidth; + } + + /** + * Return the maximum of two state values. + */ + private static @NonNull ITmfStateValue maxOf(@NonNull ITmfStateValue a, @NonNull ITmfStateValue b) { + if (a.isNull()) { + return b; + } + if (b.isNull()) { + return a; + } + if (a.getType() == Type.DOUBLE || b.getType() == Type.DOUBLE) { + return a.unboxDouble() >= b.unboxDouble() ? a : b; + } + return a.unboxLong() >= b.unboxLong() ? a : b; + } +} diff --git a/tmf/org.eclipse.tracecompass.tmf.core/src/org/eclipse/tracecompass/tmf/core/model/genericxy/AbstractTreeGenericXYCommonXDataProvider.java b/tmf/org.eclipse.tracecompass.tmf.core/src/org/eclipse/tracecompass/tmf/core/model/genericxy/AbstractTreeGenericXYCommonXDataProvider.java index 7721129d7c..60ee34a305 100644 --- a/tmf/org.eclipse.tracecompass.tmf.core/src/org/eclipse/tracecompass/tmf/core/model/genericxy/AbstractTreeGenericXYCommonXDataProvider.java +++ b/tmf/org.eclipse.tracecompass.tmf.core/src/org/eclipse/tracecompass/tmf/core/model/genericxy/AbstractTreeGenericXYCommonXDataProvider.java @@ -21,6 +21,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.eclipse.tracecompass.internal.tmf.core.model.TmfXyResponseFactory; import org.eclipse.tracecompass.internal.tmf.core.model.filters.FetchParametersUtils; +import org.eclipse.tracecompass.internal.tmf.core.statesystem.mipmap.MipmapXYQueryHelper; import org.eclipse.tracecompass.statesystem.core.ITmfStateSystem; import org.eclipse.tracecompass.statesystem.core.exceptions.StateSystemDisposedException; import org.eclipse.tracecompass.statesystem.core.exceptions.TimeRangeException; @@ -101,10 +102,17 @@ public TmfModelResponse fetchXY(Map fetchParameters // getModels returns null if the query was cancelled. return TmfXyResponseFactory.createCancelledResponse(CommonStatusMessage.TASK_CANCELLED); } + // Auto-enhance with mipmap max-per-bucket where available (time-based + // sampling only). Opt-in per provider (disabled by default). + ISampling sampling = xAxisAndYSeriesModels.getFirst(); + Collection yModels = xAxisAndYSeriesModels.getSecond(); + if (isMipmapEnhanced() && sampling instanceof ISampling.Timestamps timestamps) { + yModels = MipmapXYQueryHelper.enhanceWithMipmap(ss, getIdToQuark(), yModels, timestamps.timestamps()); + } return TmfXyResponseFactory.create( getTitle(), - xAxisAndYSeriesModels.getFirst(), - ImmutableList.copyOf(xAxisAndYSeriesModels.getSecond()), + sampling, + ImmutableList.copyOf(yModels), getDisplayType(), getXAxisDescription(), complete); diff --git a/tmf/org.eclipse.tracecompass.tmf.core/src/org/eclipse/tracecompass/tmf/core/model/tree/AbstractTreeDataProvider.java b/tmf/org.eclipse.tracecompass.tmf.core/src/org/eclipse/tracecompass/tmf/core/model/tree/AbstractTreeDataProvider.java index 4875c7cf99..4ff825154f 100644 --- a/tmf/org.eclipse.tracecompass.tmf.core/src/org/eclipse/tracecompass/tmf/core/model/tree/AbstractTreeDataProvider.java +++ b/tmf/org.eclipse.tracecompass.tmf.core/src/org/eclipse/tracecompass/tmf/core/model/tree/AbstractTreeDataProvider.java @@ -202,6 +202,40 @@ protected long getEntryId(Object key) { } } + /** + * Get the mapping from entry IDs to state system quarks. This map is + * populated during {@link #getTree} when {@link #getId(int)} is called. + * + * @return an unmodifiable view of the ID-to-quark mapping + * @since 10.2 + */ + protected BiMap getIdToQuark() { + return fIdToQuark; + } + + /** + * Whether the XY results of this provider should be enhanced with + * mipmap-based max-per-bucket downsampling (see + * {@code MipmapXYQueryHelper}). + *

+ * This is opt-in and returns {@code false} by default. It is only + * correct to enable it when every Y value returned by + * {@code getYSeriesModels} / {@code getXAxisAndYSeriesModels} is the raw + * instantaneous state value of the quark mapped through + * {@link #getIdToQuark()}. Providers that transform state values before + * returning them (for example differential counters that plot deltas, or + * CPU usage that plots a rate computed over an interval) must not + * enable this, because the maximum of the raw underlying value is not the + * maximum of the displayed value. + * + * @return {@code true} to enable mipmap enhancement, {@code false} + * otherwise. Defaults to {@code false}. + * @since 10.2 + */ + protected boolean isMipmapEnhanced() { + return false; + } + /** * Get selected entries from the filter for this provider. * diff --git a/tmf/org.eclipse.tracecompass.tmf.core/src/org/eclipse/tracecompass/tmf/core/model/xy/AbstractTreeCommonXDataProvider.java b/tmf/org.eclipse.tracecompass.tmf.core/src/org/eclipse/tracecompass/tmf/core/model/xy/AbstractTreeCommonXDataProvider.java index 42f0b4e52d..b42590b15e 100644 --- a/tmf/org.eclipse.tracecompass.tmf.core/src/org/eclipse/tracecompass/tmf/core/model/xy/AbstractTreeCommonXDataProvider.java +++ b/tmf/org.eclipse.tracecompass.tmf.core/src/org/eclipse/tracecompass/tmf/core/model/xy/AbstractTreeCommonXDataProvider.java @@ -20,6 +20,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.eclipse.tracecompass.internal.tmf.core.model.TmfXyResponseFactory; import org.eclipse.tracecompass.internal.tmf.core.model.filters.FetchParametersUtils; +import org.eclipse.tracecompass.internal.tmf.core.statesystem.mipmap.MipmapXYQueryHelper; import org.eclipse.tracecompass.statesystem.core.ITmfStateSystem; import org.eclipse.tracecompass.statesystem.core.exceptions.StateSystemDisposedException; import org.eclipse.tracecompass.statesystem.core.exceptions.TimeRangeException; @@ -93,6 +94,11 @@ public final TmfModelResponse fetchXY(Map fetchPara // getModels returns null if the query was cancelled. return TmfXyResponseFactory.createCancelledResponse(CommonStatusMessage.TASK_CANCELLED); } + // Auto-enhance with mipmap max-per-bucket where available. Opt-in + // per provider (disabled by default) via isMipmapEnhanced(). + if (isMipmapEnhanced()) { + yModels = MipmapXYQueryHelper.enhanceWithMipmap(ss, getIdToQuark(), yModels, filter.getTimesRequested()); + } return TmfXyResponseFactory.create(getTitle(), filter.getTimesRequested(), ImmutableList.copyOf(yModels), complete); } catch (StateSystemDisposedException | TimeRangeException | IndexOutOfBoundsException e) { return TmfXyResponseFactory.createFailedResponse(String.valueOf(e.getMessage()));