diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/methods.py b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/methods.py index 101c0cd425..bae891bd95 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/methods.py +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/methods.py @@ -367,7 +367,7 @@ def refresh_modules(node: sch.Schema('ModuleContainer')): # node is Module so this appears in Modules panel -@REGISTRY.method(display='Load all Modules and all Sections') +@REGISTRY.method(display='Refresh all Modules and all Sections') def load_all_sections(node: sch.Schema('Module')): """ Load/refresh all modules and all sections. diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/action/LocationTracker.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/action/LocationTracker.java index 49247ebb9d..c2edb03ef5 100644 --- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/action/LocationTracker.java +++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/action/LocationTracker.java @@ -51,8 +51,7 @@ public interface LocationTracker { * @param coordinates the trace, thread, snap, etc., of the tool * @return the address to navigate to */ - CompletableFuture
computeTraceAddress(PluginTool tool, - DebuggerCoordinates coordinates); + Address computeTraceAddress(PluginTool tool, DebuggerCoordinates coordinates); /** * Get the suggested input if the user activates "Go To" while this tracker is active diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiHandler.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiHandler.java index e7697d01b6..19c73acd5c 100644 --- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiHandler.java +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiHandler.java @@ -484,10 +484,10 @@ public class TraceRmiHandler implements TraceRmiConnection { RootMessage.Builder dispatch(RootMessage req, RootMessage.Builder rep) throws Exception; default RootMessage handle(RootMessage req) { - /*String desc = toString(req); + String desc = toString(req); if (desc != null) { TimedMsg.debug(this, "HANDLING: " + desc); - }*/ + } RootMessage.Builder rep = RootMessage.newBuilder(); try { rep = dispatch(req, rep); @@ -514,12 +514,12 @@ public class TraceRmiHandler implements TraceRmiConnection { case REQUEST_START_TX -> "startTx(%d,%s)".formatted( req.getRequestStartTx().getTxid().getId(), req.getRequestStartTx().getDescription()); - case REQUEST_SET_VALUE -> "setValue(%d,%s,%s,=%s)".formatted( + /*case REQUEST_SET_VALUE -> "setValue(%d,%s,%s,=%s)".formatted( req.getRequestSetValue().getValue().getParent().getId(), req.getRequestSetValue().getValue().getParent().getPath().getPath(), req.getRequestSetValue().getValue().getKey(), ValueDecoder.DISPLAY - .toValue(req.getRequestSetValue().getValue().getValue())); + .toValue(req.getRequestSetValue().getValue().getValue()));*/ default -> null; }; } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java index 57bded0203..855d6be8c2 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java @@ -62,6 +62,7 @@ import ghidra.util.exception.CancelledException; import resources.MultiIcon; public interface DebuggerResources { + String OPTIONS_CATEGORY_DEBUGGER = "Debugger"; String OPTIONS_CATEGORY_WORKFLOW = "Workflow"; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/DebuggerTrackLocationTrait.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/DebuggerTrackLocationTrait.java index fdffd4a399..35ff2c3d27 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/DebuggerTrackLocationTrait.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/DebuggerTrackLocationTrait.java @@ -19,7 +19,6 @@ import java.awt.Color; import java.lang.invoke.MethodHandles; import java.math.BigInteger; import java.util.*; -import java.util.concurrent.CompletableFuture; import docking.ActionContext; import docking.ComponentProvider; @@ -34,13 +33,13 @@ import ghidra.app.plugin.core.debug.gui.colors.MultiSelectionBlendedLayoutBackgr import ghidra.app.plugin.core.debug.gui.listing.DebuggerTrackedRegisterListingBackgroundColorModel; import ghidra.app.util.viewer.listingpanel.ListingBackgroundColorModel; import ghidra.app.util.viewer.listingpanel.ListingPanel; -import ghidra.async.AsyncUtils; import ghidra.debug.api.action.*; import ghidra.debug.api.action.LocationTrackingSpec.TrackingSpecConfigFieldCodec; import ghidra.debug.api.tracemgr.DebuggerCoordinates; import ghidra.framework.options.SaveState; import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.annotation.AutoConfigStateField; +import ghidra.program.model.address.Address; import ghidra.program.util.ProgramLocation; import ghidra.trace.model.*; import ghidra.trace.model.stack.TraceStack; @@ -252,7 +251,7 @@ public class DebuggerTrackLocationTrait { doTrack(); } - protected CompletableFuture computeTrackedLocation() { + protected ProgramLocation computeTrackedLocation() { // Change of register values (for current frame) // Change of stack pc (for current frame) // Change of current view (if not caused by goTo) @@ -262,16 +261,18 @@ public class DebuggerTrackLocationTrait { // Change of tracking settings DebuggerCoordinates cur = current; if (cur.getView() == null) { - return AsyncUtils.nil(); + return null; } TraceThread thread = cur.getThread(); if (thread == null || spec == null) { - return AsyncUtils.nil(); + return null; } // NB: view's snap may be forked for emulation - return tracker.computeTraceAddress(tool, cur).thenApply(address -> { - return address == null ? null : new ProgramLocation(cur.getView(), address); - }); + Address address = tracker.computeTraceAddress(tool, cur); + if (address == null) { + return null; + } + return new ProgramLocation(cur.getView(), address); } public String computeLabelText() { @@ -282,13 +283,13 @@ public class DebuggerTrackLocationTrait { } protected void doTrack() { - computeTrackedLocation().thenAccept(loc -> { - trackedLocation = loc; + try { + trackedLocation = computeTrackedLocation(); locationTracked(); - }).exceptionally(ex -> { + } + catch (Throwable ex) { Msg.error(this, "Error while computing location: " + ex); - return null; - }); + } } protected void addNewListeners() { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/NoneLocationTrackingSpec.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/NoneLocationTrackingSpec.java index e734be0cf2..30f5927a6e 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/NoneLocationTrackingSpec.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/NoneLocationTrackingSpec.java @@ -15,12 +15,9 @@ */ package ghidra.app.plugin.core.debug.gui.action; -import java.util.concurrent.CompletableFuture; - import javax.swing.Icon; import ghidra.app.plugin.core.debug.gui.DebuggerResources.TrackLocationAction; -import ghidra.async.AsyncUtils; import ghidra.debug.api.action.*; import ghidra.debug.api.tracemgr.DebuggerCoordinates; import ghidra.framework.plugintool.PluginTool; @@ -66,9 +63,8 @@ public enum NoneLocationTrackingSpec implements LocationTrackingSpec, LocationTr } @Override - public CompletableFuture
computeTraceAddress(PluginTool tool, - DebuggerCoordinates coordinates) { - return AsyncUtils.nil(); + public Address computeTraceAddress(PluginTool tool, DebuggerCoordinates coordinates) { + return null; } @Override diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/PCByStackLocationTrackingSpec.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/PCByStackLocationTrackingSpec.java index d11247ea42..900ecf4d2e 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/PCByStackLocationTrackingSpec.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/PCByStackLocationTrackingSpec.java @@ -15,8 +15,6 @@ */ package ghidra.app.plugin.core.debug.gui.action; -import java.util.concurrent.CompletableFuture; - import javax.swing.Icon; import ghidra.app.plugin.core.debug.gui.DebuggerResources.TrackLocationAction; @@ -67,7 +65,8 @@ public enum PCByStackLocationTrackingSpec implements LocationTrackingSpec, Locat return this; } - public Address doComputeTraceAddress(PluginTool tool, DebuggerCoordinates coordinates) { + @Override + public Address computeTraceAddress(PluginTool tool, DebuggerCoordinates coordinates) { Trace trace = coordinates.getTrace(); TraceThread thread = coordinates.getThread(); long snap = coordinates.getSnap(); @@ -83,16 +82,10 @@ public enum PCByStackLocationTrackingSpec implements LocationTrackingSpec, Locat return frame.getProgramCounter(snap); } - @Override - public CompletableFuture
computeTraceAddress(PluginTool tool, - DebuggerCoordinates coordinates) { - return CompletableFuture.supplyAsync(() -> doComputeTraceAddress(tool, coordinates)); - } - @Override public GoToInput getDefaultGoToInput(PluginTool tool, DebuggerCoordinates coordinates, ProgramLocation location) { - Address address = doComputeTraceAddress(tool, coordinates); + Address address = computeTraceAddress(tool, coordinates); if (address == null) { return NoneLocationTrackingSpec.INSTANCE.getDefaultGoToInput(tool, coordinates, location); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/PCLocationTrackingSpec.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/PCLocationTrackingSpec.java index c3cfdfd99d..f6aec39a48 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/PCLocationTrackingSpec.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/PCLocationTrackingSpec.java @@ -15,8 +15,6 @@ */ package ghidra.app.plugin.core.debug.gui.action; -import java.util.concurrent.CompletableFuture; - import javax.swing.Icon; import ghidra.app.plugin.core.debug.gui.DebuggerResources.TrackLocationAction; @@ -70,17 +68,14 @@ public enum PCLocationTrackingSpec implements LocationTrackingSpec, LocationTrac } @Override - public CompletableFuture
computeTraceAddress(PluginTool tool, - DebuggerCoordinates coordinates) { - return CompletableFuture.supplyAsync(() -> { - if (coordinates.getTime().isSnapOnly()) { - Address pc = BY_STACK.doComputeTraceAddress(tool, coordinates); - if (pc != null) { - return pc; - } + public Address computeTraceAddress(PluginTool tool, DebuggerCoordinates coordinates) { + if (coordinates.getTime().isSnapOnly()) { + Address pc = BY_STACK.computeTraceAddress(tool, coordinates); + if (pc != null) { + return pc; } - return BY_REG.doComputeTraceAddress(tool, coordinates); - }); + } + return BY_REG.computeTraceAddress(tool, coordinates); } @Override diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/RegisterLocationTrackingSpec.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/RegisterLocationTrackingSpec.java index 05a5ba62fe..8cf970320a 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/RegisterLocationTrackingSpec.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/RegisterLocationTrackingSpec.java @@ -15,8 +15,6 @@ */ package ghidra.app.plugin.core.debug.gui.action; -import java.util.concurrent.CompletableFuture; - import ghidra.debug.api.action.*; import ghidra.debug.api.tracemgr.DebuggerCoordinates; import ghidra.framework.plugintool.PluginTool; @@ -52,7 +50,8 @@ public interface RegisterLocationTrackingSpec extends LocationTrackingSpec, Loca return this; } - default Address doComputeTraceAddress(PluginTool tool, DebuggerCoordinates coordinates) { + @Override + default Address computeTraceAddress(PluginTool tool, DebuggerCoordinates coordinates) { Trace trace = coordinates.getTrace(); TracePlatform platform = coordinates.getPlatform(); TraceThread thread = coordinates.getThread(); @@ -88,12 +87,6 @@ public interface RegisterLocationTrackingSpec extends LocationTrackingSpec, Loca .getAddress(value.getUnsignedValue().longValue(), true)); } - @Override - default CompletableFuture
computeTraceAddress(PluginTool tool, - DebuggerCoordinates coordinates) { - return CompletableFuture.supplyAsync(() -> doComputeTraceAddress(tool, coordinates)); - } - @Override default GoToInput getDefaultGoToInput(PluginTool tool, DebuggerCoordinates coordinates, ProgramLocation location) { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/WatchLocationTrackingSpec.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/WatchLocationTrackingSpec.java index d3af101d9f..11537b05d5 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/WatchLocationTrackingSpec.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/WatchLocationTrackingSpec.java @@ -16,13 +16,11 @@ package ghidra.app.plugin.core.debug.gui.action; import java.util.Objects; -import java.util.concurrent.CompletableFuture; import javax.swing.Icon; import ghidra.app.plugin.core.debug.gui.DebuggerResources; import ghidra.app.plugin.core.debug.gui.DebuggerResources.TrackLocationAction; -import ghidra.async.AsyncUtils; import ghidra.debug.api.action.*; import ghidra.debug.api.tracemgr.DebuggerCoordinates; import ghidra.debug.api.watch.WatchRow; @@ -115,28 +113,25 @@ public class WatchLocationTrackingSpec implements LocationTrackingSpec { class WatchLocationTracker implements LocationTracker { private AddressSetView reads; private DebuggerCoordinates current = DebuggerCoordinates.NOWHERE; - private PcodeExecutor asyncExec = null; + private PcodeExecutor exec = null; private PcodeExpression compiled; @Override - public CompletableFuture
computeTraceAddress(PluginTool tool, - DebuggerCoordinates coordinates) { - if (!Objects.equals(current, coordinates) || asyncExec == null) { + public Address computeTraceAddress(PluginTool tool, DebuggerCoordinates coordinates) { + if (!Objects.equals(current, coordinates) || exec == null) { current = coordinates; - asyncExec = current.getPlatform() == null ? null + exec = current.getPlatform() == null ? null : DebuggerPcodeUtils.buildWatchExecutor(tool, coordinates); } else { - asyncExec.getState().clear(); + exec.getState().clear(); } if (current.getTrace() == null) { - return AsyncUtils.nil(); + return null; } - return CompletableFuture.supplyAsync(() -> { - compiled = DebuggerPcodeUtils.compileExpression(tool, current, expression); - WatchValue value = compiled.evaluate(asyncExec); - return value == null ? null : value.address(); - }); + compiled = DebuggerPcodeUtils.compileExpression(tool, current, expression); + WatchValue value = compiled.evaluate(exec); + return value == null ? null : value.address(); } @Override diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointsPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointsPlugin.java index f640eb7a37..4ae640a4b4 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointsPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointsPlugin.java @@ -20,7 +20,6 @@ import ghidra.app.plugin.core.debug.AbstractDebuggerPlugin; import ghidra.app.plugin.core.debug.DebuggerPluginPackage; import ghidra.app.plugin.core.debug.event.*; import ghidra.app.services.DebuggerLogicalBreakpointService; -import ghidra.app.services.DebuggerModelService; import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.util.PluginStatus; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingPlugin.java index 15a8d43fef..f6820791f4 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingPlugin.java @@ -259,48 +259,40 @@ public class DebuggerListingPlugin extends AbstractCodeBrowserPlugin { - ProgramLocationPluginEvent ev = (ProgramLocationPluginEvent) event; if (heedLocationEvent(ev)) { connectedProvider.staticProgramLocationChanged(ev.getLocation()); } }); } - if (event instanceof ProgramSelectionPluginEvent) { + if (event instanceof ProgramSelectionPluginEvent ev) { cbProgramSelectionEvents.invoke(() -> { - ProgramSelectionPluginEvent ev = (ProgramSelectionPluginEvent) event; if (heedSelectionEvent(ev)) { connectedProvider.staticProgramSelectionChanged(ev.getProgram(), ev.getSelection()); } }); } - if (event instanceof ProgramOpenedPluginEvent) { - ProgramOpenedPluginEvent ev = (ProgramOpenedPluginEvent) event; + if (event instanceof ProgramOpenedPluginEvent ev) { allProviders(p -> p.programOpened(ev.getProgram())); } - if (event instanceof ProgramClosedPluginEvent) { - ProgramClosedPluginEvent ev = (ProgramClosedPluginEvent) event; + if (event instanceof ProgramClosedPluginEvent ev) { allProviders(p -> p.programClosed(ev.getProgram())); } - if (event instanceof ProgramActivatedPluginEvent) { - ProgramActivatedPluginEvent ev = (ProgramActivatedPluginEvent) event; + if (event instanceof ProgramActivatedPluginEvent ev) { allProviders(p -> p.staticProgramActivated(ev.getActiveProgram())); } - if (event instanceof TraceActivatedPluginEvent) { - TraceActivatedPluginEvent ev = (TraceActivatedPluginEvent) event; + if (event instanceof TraceActivatedPluginEvent ev) { current = ev.getActiveCoordinates(); allProviders(p -> p.coordinatesActivated(current)); } - if (event instanceof TraceClosedPluginEvent) { - TraceClosedPluginEvent ev = (TraceClosedPluginEvent) event; + if (event instanceof TraceClosedPluginEvent ev) { if (current.getTrace() == ev.getTrace()) { current = DebuggerCoordinates.NOWHERE; } allProviders(p -> p.traceClosed(ev.getTrace())); } - // TODO: Sync selection and highlights? } void fireStaticLocationEvent(ProgramLocation staticLoc) { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProvider.java index 11b5658c48..a082272ace 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProvider.java @@ -1159,24 +1159,22 @@ public class DebuggerListingProvider extends CodeViewerProvider { } protected void doGoToTracked() { - ProgramLocation loc = trackingTrait.getTrackedLocation(); - ProgramLocation trackedStatic = doMarkTrackedLocation(); - if (loc == null) { - return; - } - TraceProgramView curView = current.getView(); - if (!syncTrait.isAutoSyncCursorWithStaticListing() || trackedStatic == null) { - Swing.runIfSwingOrRunLater(() -> { + Swing.runIfSwingOrRunLater(() -> { + ProgramLocation loc = trackingTrait.getTrackedLocation(); + ProgramLocation trackedStatic = doMarkTrackedLocation(); + if (loc == null) { + return; + } + TraceProgramView curView = current.getView(); + if (!syncTrait.isAutoSyncCursorWithStaticListing() || trackedStatic == null) { if (curView != current.getView()) { // Trace changed before Swing scheduled us return; } goToAndUpdateTrackingLabel(curView, loc); doCheckCurrentModuleMissing(); - }); - } - else { - Swing.runIfSwingOrRunLater(() -> { + } + else { if (curView != current.getView()) { // Trace changed before Swing scheduled us return; @@ -1184,8 +1182,8 @@ public class DebuggerListingProvider extends CodeViewerProvider { goToAndUpdateTrackingLabel(curView, loc); doCheckCurrentModuleMissing(); plugin.fireStaticLocationEvent(trackedStatic); - }); - } + } + }); } protected void doAutoDisassemble(Address start) { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerMemoryBytesPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerMemoryBytesPlugin.java index bb9cc4e6dd..97354bfb58 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerMemoryBytesPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerMemoryBytesPlugin.java @@ -150,13 +150,11 @@ public class DebuggerMemoryBytesPlugin @Override public void processEvent(PluginEvent event) { - if (event instanceof TraceActivatedPluginEvent) { - TraceActivatedPluginEvent ev = (TraceActivatedPluginEvent) event; + if (event instanceof TraceActivatedPluginEvent ev) { current = ev.getActiveCoordinates(); allProviders(p -> p.coordinatesActivated(current)); } - if (event instanceof TraceClosedPluginEvent) { - TraceClosedPluginEvent ev = (TraceClosedPluginEvent) event; + if (event instanceof TraceClosedPluginEvent ev) { if (current.getTrace() == ev.getTrace()) { current = DebuggerCoordinates.NOWHERE; } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionsPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionsPlugin.java index 934fcd56a1..ae00ec4a73 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionsPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionsPlugin.java @@ -66,20 +66,16 @@ public class DebuggerRegionsPlugin extends AbstractDebuggerPlugin { @Override public void processEvent(PluginEvent event) { super.processEvent(event); - if (event instanceof ProgramActivatedPluginEvent) { - ProgramActivatedPluginEvent ev = (ProgramActivatedPluginEvent) event; + if (event instanceof ProgramActivatedPluginEvent ev) { provider.setProgram(ev.getActiveProgram()); } - else if (event instanceof ProgramLocationPluginEvent) { - ProgramLocationPluginEvent ev = (ProgramLocationPluginEvent) event; + else if (event instanceof ProgramLocationPluginEvent ev) { provider.setLocation(ev.getLocation()); } - else if (event instanceof ProgramClosedPluginEvent) { - ProgramClosedPluginEvent ev = (ProgramClosedPluginEvent) event; + else if (event instanceof ProgramClosedPluginEvent ev) { provider.programClosed(ev.getProgram()); } - else if (event instanceof TraceActivatedPluginEvent) { - TraceActivatedPluginEvent ev = (TraceActivatedPluginEvent) event; + else if (event instanceof TraceActivatedPluginEvent ev) { provider.coordinatesActivated(ev.getActiveCoordinates()); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memview/DebuggerMemviewPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memview/DebuggerMemviewPlugin.java index 2244a5036c..1e4466ba89 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memview/DebuggerMemviewPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memview/DebuggerMemviewPlugin.java @@ -26,22 +26,21 @@ import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.util.PluginStatus; import ghidra.program.model.listing.Program; -@PluginInfo( // - shortDescription = "Displays memory vs time", // - description = "Provides visualiztion/navigation across time/address axes", // - category = PluginCategoryNames.DEBUGGER, // - packageName = DebuggerPluginPackage.NAME, // - status = PluginStatus.RELEASED, // - eventsConsumed = { // - TraceActivatedPluginEvent.class // - }, // - servicesRequired = { // - DebuggerTraceManagerService.class // - }, // - servicesProvided = { // - MemviewService.class // - } // -) +@PluginInfo( + shortDescription = "Displays memory vs time", + description = "Provides visualiztion/navigation across time/address axes", + category = PluginCategoryNames.DEBUGGER, + packageName = DebuggerPluginPackage.NAME, + status = PluginStatus.RELEASED, + eventsConsumed = { + TraceActivatedPluginEvent.class + }, + servicesRequired = { + DebuggerTraceManagerService.class + }, + servicesProvided = { + MemviewService.class + }) public class DebuggerMemviewPlugin extends AbstractDebuggerPlugin implements MemviewService { protected MemviewProvider provider; @@ -67,12 +66,12 @@ public class DebuggerMemviewPlugin extends AbstractDebuggerPlugin implements Mem @Override public void processEvent(PluginEvent event) { super.processEvent(event); - if (event instanceof TraceActivatedPluginEvent) { - TraceActivatedPluginEvent ev = (TraceActivatedPluginEvent) event; + if (event instanceof TraceActivatedPluginEvent ev) { listener.coordinatesActivated(ev.getActiveCoordinates()); } } + @Override public MemviewProvider getProvider() { return provider; } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesPlugin.java index c8412106e0..224d361b88 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesPlugin.java @@ -65,20 +65,16 @@ public class DebuggerModulesPlugin extends AbstractDebuggerPlugin { @Override public void processEvent(PluginEvent event) { super.processEvent(event); - if (event instanceof ProgramActivatedPluginEvent) { - ProgramActivatedPluginEvent ev = (ProgramActivatedPluginEvent) event; + if (event instanceof ProgramActivatedPluginEvent ev) { provider.setProgram(ev.getActiveProgram()); } - else if (event instanceof ProgramLocationPluginEvent) { - ProgramLocationPluginEvent ev = (ProgramLocationPluginEvent) event; + else if (event instanceof ProgramLocationPluginEvent ev) { provider.setLocation(ev.getLocation()); } - else if (event instanceof ProgramClosedPluginEvent) { - ProgramClosedPluginEvent ev = (ProgramClosedPluginEvent) event; + else if (event instanceof ProgramClosedPluginEvent ev) { provider.programClosed(ev.getProgram()); } - else if (event instanceof TraceActivatedPluginEvent) { - TraceActivatedPluginEvent ev = (TraceActivatedPluginEvent) event; + else if (event instanceof TraceActivatedPluginEvent ev) { provider.coordinatesActivated(ev.getActiveCoordinates()); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerStaticMappingPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerStaticMappingPlugin.java index ac892eaefc..de46e9557c 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerStaticMappingPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerStaticMappingPlugin.java @@ -64,12 +64,10 @@ public class DebuggerStaticMappingPlugin extends AbstractDebuggerPlugin { @Override public void processEvent(PluginEvent event) { super.processEvent(event); - if (event instanceof TraceActivatedPluginEvent) { - TraceActivatedPluginEvent ev = (TraceActivatedPluginEvent) event; + if (event instanceof TraceActivatedPluginEvent ev) { provider.setTrace(ev.getActiveCoordinates().getTrace()); } - if (event instanceof ProgramActivatedPluginEvent) { - ProgramActivatedPluginEvent ev = (ProgramActivatedPluginEvent) event; + if (event instanceof ProgramActivatedPluginEvent ev) { provider.setProgram(ev.getActiveProgram()); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsPlugin.java index 2ca4f917d9..66610a3c18 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsPlugin.java @@ -112,38 +112,32 @@ public class DebuggerObjectsPlugin extends AbstractDebuggerPlugin provider.traceOpened(ev.getTrace()); } } - else if (event instanceof TraceActivatedPluginEvent) { - TraceActivatedPluginEvent ev = (TraceActivatedPluginEvent) event; + else if (event instanceof TraceActivatedPluginEvent ev) { for (DebuggerObjectsProvider provider : providers) { provider.traceActivated(ev.getActiveCoordinates()); } } - else if (event instanceof TraceClosedPluginEvent) { - TraceClosedPluginEvent ev = (TraceClosedPluginEvent) event; + else if (event instanceof TraceClosedPluginEvent ev) { for (DebuggerObjectsProvider provider : providers) { provider.traceClosed(ev.getTrace()); } } - else if (event instanceof ModelActivatedPluginEvent) { - ModelActivatedPluginEvent ev = (ModelActivatedPluginEvent) event; + else if (event instanceof ModelActivatedPluginEvent ev) { for (DebuggerObjectsProvider provider : providers) { provider.modelActivated(ev.getActiveModel()); } } - else if (event instanceof ProgramActivatedPluginEvent) { - ProgramActivatedPluginEvent ev = (ProgramActivatedPluginEvent) event; + else if (event instanceof ProgramActivatedPluginEvent ev) { for (DebuggerObjectsProvider provider : providers) { provider.setProgram(ev.getActiveProgram()); } } - else if (event instanceof ProgramOpenedPluginEvent) { - ProgramOpenedPluginEvent ev = (ProgramOpenedPluginEvent) event; + else if (event instanceof ProgramOpenedPluginEvent ev) { for (DebuggerObjectsProvider provider : providers) { provider.setProgram(ev.getProgram()); } } - else if (event instanceof ProgramSelectionPluginEvent) { - ProgramSelectionPluginEvent ev = (ProgramSelectionPluginEvent) event; + else if (event instanceof ProgramSelectionPluginEvent ev) { for (DebuggerObjectsProvider provider : providers) { provider.setProgram(ev.getProgram()); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/pcode/DebuggerPcodeStepperPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/pcode/DebuggerPcodeStepperPlugin.java index a3ba6f59da..e6a7f66af8 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/pcode/DebuggerPcodeStepperPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/pcode/DebuggerPcodeStepperPlugin.java @@ -53,8 +53,7 @@ public class DebuggerPcodeStepperPlugin extends AbstractDebuggerPlugin { @Override public void processEvent(PluginEvent event) { super.processEvent(event); - if (event instanceof TraceActivatedPluginEvent) { - TraceActivatedPluginEvent ev = (TraceActivatedPluginEvent) event; + if (event instanceof TraceActivatedPluginEvent ev) { provider.coordinatesActivated(ev.getActiveCoordinates()); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/platform/DebuggerPlatformPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/platform/DebuggerPlatformPlugin.java index 57220d4710..a7f79e2a08 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/platform/DebuggerPlatformPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/platform/DebuggerPlatformPlugin.java @@ -29,7 +29,8 @@ import ghidra.app.plugin.PluginCategoryNames; import ghidra.app.plugin.core.debug.DebuggerPluginPackage; import ghidra.app.plugin.core.debug.event.*; import ghidra.app.plugin.core.debug.gui.DebuggerResources; -import ghidra.app.plugin.core.debug.mapping.*; +import ghidra.app.plugin.core.debug.mapping.DebuggerPlatformOffer; +import ghidra.app.plugin.core.debug.mapping.DebuggerPlatformOpinion; import ghidra.app.services.DebuggerPlatformService; import ghidra.app.services.DebuggerTraceManagerService; import ghidra.debug.api.platform.DebuggerPlatformMapper; @@ -307,14 +308,14 @@ public class DebuggerPlatformPlugin extends Plugin { @Override public void processEvent(PluginEvent event) { super.processEvent(event); - if (event instanceof TraceActivatedPluginEvent evt) { - coordinatesActivated(evt.getActiveCoordinates()); + if (event instanceof TraceActivatedPluginEvent ev) { + coordinatesActivated(ev.getActiveCoordinates()); } - if (event instanceof TraceClosedPluginEvent evt) { - traceClosed(evt.getTrace()); + if (event instanceof TraceClosedPluginEvent ev) { + traceClosed(ev.getTrace()); } - if (event instanceof DebuggerPlatformPluginEvent evt) { - mapperActivated(evt.getTrace(), evt.getMapper()); + if (event instanceof DebuggerPlatformPluginEvent ev) { + mapperActivated(ev.getTrace(), ev.getMapper()); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersPlugin.java index 2c003a71d7..8f674497cb 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersPlugin.java @@ -101,12 +101,10 @@ public class DebuggerRegistersPlugin extends AbstractDebuggerPlugin { @Override public void processEvent(PluginEvent event) { super.processEvent(event); - if (event instanceof TraceActivatedPluginEvent) { - TraceActivatedPluginEvent ev = (TraceActivatedPluginEvent) event; + if (event instanceof TraceActivatedPluginEvent ev) { connectedProvider.coordinatesActivated(ev.getActiveCoordinates()); } - if (event instanceof TraceClosedPluginEvent) { - TraceClosedPluginEvent ev = (TraceClosedPluginEvent) event; + if (event instanceof TraceClosedPluginEvent ev) { traceClosed(ev.getTrace()); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsPlugin.java index 11e97c7c5e..5fe9bc664b 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsPlugin.java @@ -18,7 +18,8 @@ package ghidra.app.plugin.core.debug.gui.thread; import ghidra.app.plugin.PluginCategoryNames; import ghidra.app.plugin.core.debug.AbstractDebuggerPlugin; import ghidra.app.plugin.core.debug.DebuggerPluginPackage; -import ghidra.app.plugin.core.debug.event.*; +import ghidra.app.plugin.core.debug.event.TraceActivatedPluginEvent; +import ghidra.app.plugin.core.debug.event.TraceOpenedPluginEvent; import ghidra.app.services.DebuggerTraceManagerService; import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.util.PluginStatus; @@ -57,8 +58,7 @@ public class DebuggerThreadsPlugin extends AbstractDebuggerPlugin { @Override public void processEvent(PluginEvent event) { super.processEvent(event); - if (event instanceof TraceActivatedPluginEvent) { - TraceActivatedPluginEvent ev = (TraceActivatedPluginEvent) event; + if (event instanceof TraceActivatedPluginEvent ev) { provider.coordinatesActivated(ev.getActiveCoordinates()); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerSnapshotTablePanel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerSnapshotTablePanel.java index f2bf24b6ec..20c5af7659 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerSnapshotTablePanel.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerSnapshotTablePanel.java @@ -21,6 +21,7 @@ import java.util.Collection; import java.util.Objects; import java.util.function.BiConsumer; import java.util.function.Function; +import java.util.stream.Collectors; import javax.swing.*; import javax.swing.table.*; @@ -141,7 +142,7 @@ public class DebuggerSnapshotTablePanel extends JPanel { public Component getTableCellRendererComponent(GTableCellRenderingData data) { super.getTableCellRendererComponent(data); SnapshotRow row = (SnapshotRow) data.getRowObject(); - if (row != null && row.getSnap() == currentSnap) { + if (row != null && currentSnap != null && currentSnap.longValue() == row.getSnap()) { setBold(); } return this; @@ -154,7 +155,7 @@ public class DebuggerSnapshotTablePanel extends JPanel { protected boolean hideScratch = true; private Trace currentTrace; - private Long currentSnap; + private volatile Long currentSnap; protected final SnapshotListener listener = new SnapshotListener(); @@ -237,8 +238,11 @@ public class DebuggerSnapshotTablePanel extends JPanel { Collection snapshots = hideScratch ? manager.getSnapshots(0, true, Long.MAX_VALUE, true) : manager.getAllSnapshots(); - snapshotTableModel - .addAll(snapshots.stream().map(s -> new SnapshotRow(currentTrace, s)).toList()); + // Use .collect instead of .toList to avoid size/sync issues + // Even though access is synchronized, size may change during iteration + snapshotTableModel.addAll(snapshots.stream() + .map(s -> new SnapshotRow(currentTrace, s)) + .collect(Collectors.toList())); } protected void deleteScratchSnapshots() { @@ -250,10 +254,11 @@ public class DebuggerSnapshotTablePanel extends JPanel { return; } TraceTimeManager manager = currentTrace.getTimeManager(); - snapshotTableModel.addAll(manager.getSnapshots(Long.MIN_VALUE, true, 0, false) - .stream() + Collection sratch = + manager.getSnapshots(Long.MIN_VALUE, true, 0, false); + snapshotTableModel.addAll(sratch.stream() .map(s -> new SnapshotRow(currentTrace, s)) - .toList()); + .collect(Collectors.toList())); } public ListSelectionModel getSelectionModel() { @@ -265,8 +270,12 @@ public class DebuggerSnapshotTablePanel extends JPanel { return row == null ? null : row.getSnap(); } - public void setSelectedSnapshot(Long snap) { + public void setCurrentSnapshot(Long snap) { currentSnap = snap; + snapshotTableModel.fireTableDataChanged(); + } + + public void setSelectedSnapshot(Long snap) { if (snap == null) { snapshotTable.clearSelection(); return; @@ -283,6 +292,5 @@ public class DebuggerSnapshotTablePanel extends JPanel { return; } snapshotFilterPanel.setSelectedItem(row); - snapshotTableModel.fireTableDataChanged(); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimePlugin.java index a6ab6d4765..a543b6eeee 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimePlugin.java @@ -122,8 +122,7 @@ public class DebuggerTimePlugin extends AbstractDebuggerPlugin { @Override public void processEvent(PluginEvent event) { super.processEvent(event); - if (event instanceof TraceActivatedPluginEvent) { - TraceActivatedPluginEvent ev = (TraceActivatedPluginEvent) event; + if (event instanceof TraceActivatedPluginEvent ev) { provider.coordinatesActivated(ev.getActiveCoordinates()); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimeProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimeProvider.java index 0ece8829a2..b08d6b812b 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimeProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimeProvider.java @@ -209,7 +209,7 @@ public class DebuggerTimeProvider extends ComponentProviderAdapter { current = coordinates; mainPanel.setTrace(current.getTrace()); - mainPanel.setSelectedSnapshot(current.getSnap()); + mainPanel.setCurrentSnapshot(current.getSnap()); } public void writeConfigState(SaveState saveState) { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/DebuggerLogicalBreakpointServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/DebuggerLogicalBreakpointServicePlugin.java index d13acb7d13..15b8b6b64d 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/DebuggerLogicalBreakpointServicePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/DebuggerLogicalBreakpointServicePlugin.java @@ -1360,23 +1360,23 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin @Override public void processEvent(PluginEvent event) { - if (event instanceof ProgramOpenedPluginEvent evt) { - programOpened(evt.getProgram()); + if (event instanceof ProgramOpenedPluginEvent ev) { + programOpened(ev.getProgram()); } - else if (event instanceof ProgramClosedPluginEvent evt) { - programClosed(evt.getProgram()); + else if (event instanceof ProgramClosedPluginEvent ev) { + programClosed(ev.getProgram()); } - else if (event instanceof TraceOpenedPluginEvent evt) { - traceOpened(evt.getTrace()); + else if (event instanceof TraceOpenedPluginEvent ev) { + traceOpened(ev.getTrace()); } - else if (event instanceof TraceActivatedPluginEvent evt) { - traceSnapChanged(evt.getActiveCoordinates()); + else if (event instanceof TraceActivatedPluginEvent ev) { + traceSnapChanged(ev.getActiveCoordinates()); } - else if (event instanceof TraceInactiveCoordinatesPluginEvent evt) { - traceSnapChanged(evt.getCoordinates()); + else if (event instanceof TraceInactiveCoordinatesPluginEvent ev) { + traceSnapChanged(ev.getCoordinates()); } - else if (event instanceof TraceClosedPluginEvent evt) { - traceClosed(evt.getTrace()); + else if (event instanceof TraceClosedPluginEvent ev) { + traceClosed(ev.getTrace()); } } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/control/DebuggerControlServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/control/DebuggerControlServicePlugin.java index 4b9b2bbba6..0ed085db2b 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/control/DebuggerControlServicePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/control/DebuggerControlServicePlugin.java @@ -20,7 +20,8 @@ import java.util.*; import java.util.concurrent.*; import ghidra.app.plugin.PluginCategoryNames; -import ghidra.app.plugin.core.debug.*; +import ghidra.app.plugin.core.debug.AbstractDebuggerPlugin; +import ghidra.app.plugin.core.debug.DebuggerPluginPackage; import ghidra.app.plugin.core.debug.event.*; import ghidra.app.services.*; import ghidra.app.services.DebuggerTraceManagerService.ActivationCause; @@ -342,14 +343,14 @@ public class DebuggerControlServicePlugin extends AbstractDebuggerPlugin @Override public void processEvent(PluginEvent event) { super.processEvent(event); - if (event instanceof TraceOpenedPluginEvent evt) { - installAllMemoryEditors(evt.getTrace()); + if (event instanceof TraceOpenedPluginEvent ev) { + installAllMemoryEditors(ev.getTrace()); } - else if (event instanceof TraceActivatedPluginEvent evt) { - coordinatesActivated(evt.getActiveCoordinates(), evt.getCause()); + else if (event instanceof TraceActivatedPluginEvent ev) { + coordinatesActivated(ev.getActiveCoordinates(), ev.getCause()); } - else if (event instanceof TraceClosedPluginEvent evt) { - uninstallAllMemoryEditors(evt.getTrace()); + else if (event instanceof TraceClosedPluginEvent ev) { + uninstallAllMemoryEditors(ev.getTrace()); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServicePlugin.java index 1ce22be55f..c57aa2173b 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServicePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServicePlugin.java @@ -83,6 +83,24 @@ public class DebuggerStaticMappingServicePlugin extends Plugin this.mapping = mapping; } + @Override + public boolean equals(Object o) { + if (!(o instanceof MappingEntry that)) { + return false; + } + // Yes, use identity, since it should be the same trace db records + if (this.mapping != that.mapping) { + return false; + } + if (this.program != that.program) { + return false; + } + if (!Objects.equals(this.staticRange, that.staticRange)) { + return false; + } + return true; + } + public Trace getTrace() { return mapping.getTrace(); } @@ -232,11 +250,14 @@ public class DebuggerStaticMappingServicePlugin extends Plugin private void objectRestored() { synchronized (lock) { - doAffectedByTraceClosed(trace); + var old = Map.copyOf(outbound); outbound.clear(); loadOutboundEntries(); // Also places/updates corresponding inbound entries - // TODO: What about removed corresponding inbound entries? - doAffectedByTraceOpened(trace); + if (!old.equals(outbound)) { + // TODO: What about removed corresponding inbound entries? + doAffectedByTraceClosed(trace); + doAffectedByTraceOpened(trace); + } } } @@ -811,13 +832,13 @@ public class DebuggerStaticMappingServicePlugin extends Plugin } protected T noTraceInfo() { - Msg.warn(this, "The given trace is not open in this tool " + + Msg.debug(this, "The given trace is not open in this tool " + "(or the service hasn't received and processed the open-trace event, yet)"); return null; } protected T noProgramInfo() { - Msg.warn(this, "The given program is not open in this tool " + + Msg.debug(this, "The given program is not open in this tool " + "(or the service hasn't received and processed the open-program event, yet)"); return null; } diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionsProviderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionsProviderTest.java index 0045c9843e..ec84b59533 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionsProviderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionsProviderTest.java @@ -269,7 +269,7 @@ public class DebuggerRegionsProviderTest extends AbstractGhidraHeadedDebuggerTes waitForPass(() -> assertTableSize(0)); } - @Test + // @Test // Not gonna with write-behind cache public void testUndoRedo() throws Exception { createAndOpenTrace(); @@ -304,7 +304,7 @@ public class DebuggerRegionsProviderTest extends AbstractGhidraHeadedDebuggerTes }); } - @Test + // @Test // Not gonna with write-behind cache public void testAbort() throws Exception { createAndOpenTrace(); traceManager.activateTrace(tb.trace); diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelProviderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelProviderTest.java index 4122d4952f..9c5508ab41 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelProviderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelProviderTest.java @@ -1128,7 +1128,8 @@ public class DebuggerModelProviderTest extends AbstractGhidraHeadedDebuggerTest } waitForTasks(); - assertEquals(4, modelProvider.attributesTablePanel.tableModel.getModelData().size()); + waitForPass(() -> assertEquals(4, + modelProvider.attributesTablePanel.tableModel.getModelData().size())); } @Test @@ -1158,6 +1159,7 @@ public class DebuggerModelProviderTest extends AbstractGhidraHeadedDebuggerTest // TODO: Should I collapse entries that are links to the same object? // Would use the "Life" column to display span for each included entry. // Neat, but not sure it's worth it - assertEquals(14, modelProvider.elementsTablePanel.tableModel.getModelData().size()); + waitForPass(() -> assertEquals(14, + modelProvider.elementsTablePanel.tableModel.getModelData().size())); } } diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProviderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProviderTest.java index 6884a48537..0c6302b72c 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProviderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProviderTest.java @@ -401,7 +401,7 @@ public class DebuggerModulesProviderTest extends AbstractGhidraHeadedDebuggerTes }); } - @Test + // @Test // Not gonna with write-behind cache public void testUndoRedoCausesUpdateInProvider() throws Exception { createAndOpenTrace(); diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProviderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProviderTest.java index 71d9a313f6..39c9ac0c36 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProviderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProviderTest.java @@ -444,7 +444,7 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerTes thread1.getObject().getAttribute(0, TraceObjectThread.KEY_COMMENT).getValue())); } - @Test + // @Test // Not gonna with write-behind cache public void testUndoRedoCausesUpdateInProvider() throws Exception { createAndOpenTrace(); addThreads(); diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimeProviderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimeProviderTest.java index c93a456344..a3f9e7fbbf 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimeProviderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimeProviderTest.java @@ -15,7 +15,8 @@ */ package ghidra.app.plugin.core.debug.gui.time; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import java.util.Calendar; import java.util.List; @@ -286,35 +287,7 @@ public class DebuggerTimeProviderTest extends AbstractGhidraHeadedDebuggerTest { tb.trace.getTimeManager().getSnapshot(0, false).getDescription()); } - @Test - public void testActivateSnapSelectsRow() throws Exception { - createSnaplessTrace(); - traceManager.openTrace(tb.trace); - addSnapshots(); - waitForDomainObject(tb.trace); - - assertProviderEmpty(); - - traceManager.activateTrace(tb.trace); - waitForSwing(); - - List data = timeProvider.mainPanel.snapshotTableModel.getModelData(); - - traceManager.activateSnap(0); - waitForSwing(); - - assertEquals(data.get(0), timeProvider.mainPanel.snapshotFilterPanel.getSelectedItem()); - - traceManager.activateSnap(10); - waitForSwing(); - - assertEquals(data.get(1), timeProvider.mainPanel.snapshotFilterPanel.getSelectedItem()); - - traceManager.activateSnap(5); - waitForSwing(); - - assertNull(timeProvider.mainPanel.snapshotFilterPanel.getSelectedItem()); - } + // TODO: Test activation bolds the row @Test public void testDoubleClickRowActivatesSnap() throws Exception { diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/record/ObjectBasedTraceRecorderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/record/ObjectBasedTraceRecorderTest.java index c6f8f5c460..015185ec33 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/record/ObjectBasedTraceRecorderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/record/ObjectBasedTraceRecorderTest.java @@ -47,6 +47,7 @@ import ghidra.trace.model.target.*; import ghidra.trace.model.thread.*; import ghidra.trace.model.time.TraceSnapshot; import ghidra.trace.model.time.TraceTimeManager; +import ghidra.util.StreamUtils; import ghidra.util.task.TaskMonitor; public class ObjectBasedTraceRecorderTest extends AbstractGhidraHeadedDebuggerTest { @@ -88,7 +89,7 @@ public class ObjectBasedTraceRecorderTest extends AbstractGhidraHeadedDebuggerTe protected void dumpObjects() { System.err.println("All objects:"); - for (TraceObject object : objects.getAllObjects()) { + for (TraceObject object : StreamUtils.iter(objects.getAllObjects())) { System.err.println(" " + object); } } @@ -99,7 +100,7 @@ public class ObjectBasedTraceRecorderTest extends AbstractGhidraHeadedDebuggerTe waitForPass(noExc(() -> { waitOn(recorder.flushTransactions()); - assertEquals(5, objects.getAllObjects().size()); + assertEquals(5, objects.getObjectCount()); })); } diff --git a/Ghidra/Debug/Framework-AsyncComm/src/main/java/ghidra/async/AsyncDebouncer.java b/Ghidra/Debug/Framework-AsyncComm/src/main/java/ghidra/async/AsyncDebouncer.java index 2a9e66da7a..db104ab380 100644 --- a/Ghidra/Debug/Framework-AsyncComm/src/main/java/ghidra/async/AsyncDebouncer.java +++ b/Ghidra/Debug/Framework-AsyncComm/src/main/java/ghidra/async/AsyncDebouncer.java @@ -32,6 +32,8 @@ import java.util.function.Consumer; * debouncer configured with a time window that contains all the events, only the final event in the * cluster will be processed. The cost of doing this is a waiting period, so event processing may be * less responsive, but will also be less frantic. + * + * @param the value type */ public class AsyncDebouncer { protected final AsyncTimer timer; @@ -99,7 +101,7 @@ public class AsyncDebouncer { * This sets or resets the timer for the event window. The settled event will fire with the * given value after this waiting period, unless another contact event occurs first. * - * @param val + * @param val the new value */ public synchronized void contact(T val) { lastContact = val; @@ -141,4 +143,16 @@ public class AsyncDebouncer { } return settled(); } + + public static class Bypass extends AsyncDebouncer { + public Bypass() { + super(null, 0); + } + + @Override + public synchronized void contact(T val) { + lastContact = val; + doSettled(); + } + } } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/DBTrace.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/DBTrace.java index e1b47a5966..8e820e3e74 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/DBTrace.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/DBTrace.java @@ -15,6 +15,7 @@ */ package ghidra.trace.database; +import java.io.File; import java.io.IOException; import java.util.*; import java.util.function.Consumer; @@ -149,6 +150,8 @@ public class DBTrace extends DBCachedDomainObjectAdapter implements Trace, Trace protected ListenerSet viewListeners = new ListenerSet<>(TraceProgramViewListener.class, true); + private volatile boolean closing; + public DBTrace(String name, CompilerSpec baseCompilerSpec, Object consumer) throws IOException, LanguageNotFoundException { super(new DBHandle(), DBOpenMode.CREATE, TaskMonitor.DUMMY, name, DB_TIME_INTERVAL, @@ -608,7 +611,7 @@ public class DBTrace extends DBCachedDomainObjectAdapter implements Trace, Trace DBTraceProgramView view; try (LockHold hold = lockRead()) { view = fixedProgramViews.computeIfAbsent(snap, s -> { - Msg.debug(this, "Creating fixed view at snap=" + snap); + Msg.trace(this, "Creating fixed view at snap=" + snap); return new DBTraceProgramView(this, snap, baseCompilerSpec); }); } @@ -871,4 +874,29 @@ public class DBTrace extends DBCachedDomainObjectAdapter implements Trace, Trace public void updateViewportsSnapshotDeleted(TraceSnapshot snapshot) { allViewports(v -> v.updateSnapshotDeleted(snapshot)); } + + @Override + public void save(String comment, TaskMonitor monitor) throws IOException, CancelledException { + objectManager.flushWbCaches(); + super.save(comment, monitor); + } + + @Override + public void saveToPackedFile(File outputFile, TaskMonitor monitor) + throws IOException, CancelledException { + objectManager.flushWbCaches(); + super.saveToPackedFile(outputFile, monitor); + } + + public boolean isClosing() { + return closing; + } + + @Override + protected void close() { + closing = true; + objectManager.flushWbCaches(); + super.close(); + objectManager.waitWbWorkers(); + } } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/AbstractDBTraceProgramViewMemory.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/AbstractDBTraceProgramViewMemory.java index 3f74c8fc34..847aef024c 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/AbstractDBTraceProgramViewMemory.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/AbstractDBTraceProgramViewMemory.java @@ -49,6 +49,21 @@ public abstract class AbstractDBTraceProgramViewMemory protected LiveMemoryHandler memoryWriteRedirect; + private static final int CACHE_PAGE_COUNT = 3; + protected final ByteCache cache = new ByteCache(CACHE_PAGE_COUNT) { + @Override + protected int doLoad(Address address, ByteBuffer buf) throws MemoryAccessException { + DBTraceMemorySpace space = + program.trace.getMemoryManager().getMemorySpace(address.getAddressSpace(), false); + if (space == null) { + int len = buf.remaining(); + buf.position(buf.limit()); + return len; + } + return space.getViewBytes(program.snap, address, buf); + } + }; + public AbstractDBTraceProgramViewMemory(DBTraceProgramView program) { this.program = program; this.memoryManager = program.trace.getMemoryManager(); @@ -301,24 +316,25 @@ public abstract class AbstractDBTraceProgramViewMemory @Override public byte getByte(Address addr) throws MemoryAccessException { - MemoryBlock block = getBlock(addr); - if (block == null) { - return 0; // Memory assumed initialized to 0 + try (LockHold hold = program.trace.lockRead()) { + return cache.read(addr); } - return block.getByte(addr); } @Override - public int getBytes(Address addr, byte[] dest, int destIndex, int size) - throws MemoryAccessException { - MemoryBlock block = getBlock(addr); - if (block == null) { - int avail = MathUtilities.unsignedMin(Math.max(0, size), - addr.getAddressSpace().getMaxAddress().subtract(addr)); - Arrays.fill(dest, destIndex, avail, (byte) 0); - return avail; + public int getBytes(Address addr, byte[] b, int off, int len) throws MemoryAccessException { + try (LockHold hold = program.trace.lockRead()) { + if (cache.canCache(addr, len)) { + return cache.read(addr, ByteBuffer.wrap(b, off, len)); + } + AddressSpace as = addr.getAddressSpace(); + DBTraceMemorySpace space = program.trace.getMemoryManager().getMemorySpace(as, false); + if (space == null) { + throw new MemoryAccessException("Space does not exist"); + } + len = MathUtilities.unsignedMin(len, as.getMaxAddress().subtract(addr) + 1); + return space.getViewBytes(program.snap, addr, ByteBuffer.wrap(b, off, len)); } - return block.getBytes(addr, dest, destIndex, size); } @Override diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/AbstractDBTraceProgramViewMemoryBlock.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/AbstractDBTraceProgramViewMemoryBlock.java index 9c89f48518..9a4d0c0128 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/AbstractDBTraceProgramViewMemoryBlock.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/AbstractDBTraceProgramViewMemoryBlock.java @@ -26,7 +26,6 @@ import ghidra.program.model.address.*; import ghidra.program.model.mem.*; import ghidra.trace.database.memory.DBTraceMemorySpace; import ghidra.trace.model.memory.TraceMemorySpaceInputStream; -import ghidra.util.LockHold; import ghidra.util.MathUtilities; public abstract class AbstractDBTraceProgramViewMemoryBlock implements MemoryBlock { @@ -97,19 +96,6 @@ public abstract class AbstractDBTraceProgramViewMemoryBlock implements MemoryBlo private final List info = Collections.singletonList(new MyMemoryBlockSourceInfo()); - private static final int CACHE_PAGE_COUNT = 3; - private final ByteCache cache = new ByteCache(CACHE_PAGE_COUNT) { - @Override - protected int doLoad(Address address, ByteBuffer buf) throws MemoryAccessException { - DBTraceMemorySpace space = - program.trace.getMemoryManager().getMemorySpace(getAddressSpace(), false); - if (space == null) { - throw new MemoryAccessException("Space does not exist"); - } - return space.getViewBytes(program.snap, address, buf); - } - }; - protected AbstractDBTraceProgramViewMemoryBlock(DBTraceProgramView program) { this.program = program; } @@ -120,15 +106,6 @@ public abstract class AbstractDBTraceProgramViewMemoryBlock implements MemoryBlo return getStart().getAddressSpace(); } - /** - * Should be called when the snap changes or when bytes change - */ - protected void invalidateBytesCache(AddressRange range) { - if (range == null || range.intersects(getAddressRange())) { - cache.invalidate(range); - } - } - protected DBTraceMemorySpace getMemorySpace() { return program.trace.getMemoryManager().getMemorySpace(getAddressSpace(), false); } @@ -191,13 +168,11 @@ public abstract class AbstractDBTraceProgramViewMemoryBlock implements MemoryBlo @Override public byte getByte(Address addr) throws MemoryAccessException { - try (LockHold hold = program.trace.lockRead()) { - AddressRange range = getAddressRange(); - if (!range.contains(addr)) { - throw new MemoryAccessException(); - } - return cache.read(addr); + AddressRange range = getAddressRange(); + if (!range.contains(addr)) { + throw new MemoryAccessException(); } + return program.memory.getByte(addr); } @Override @@ -207,22 +182,12 @@ public abstract class AbstractDBTraceProgramViewMemoryBlock implements MemoryBlo @Override public int getBytes(Address addr, byte[] b, int off, int len) throws MemoryAccessException { - try (LockHold hold = program.trace.lockRead()) { - AddressRange range = getAddressRange(); - if (!range.contains(addr)) { - throw new MemoryAccessException(); - } - if (cache.canCache(addr, len)) { - return cache.read(addr, ByteBuffer.wrap(b, off, len)); - } - DBTraceMemorySpace space = - program.trace.getMemoryManager().getMemorySpace(range.getAddressSpace(), false); - if (space == null) { - throw new MemoryAccessException("Space does not exist"); - } - len = MathUtilities.unsignedMin(len, range.getMaxAddress().subtract(addr) + 1); - return space.getViewBytes(program.snap, addr, ByteBuffer.wrap(b, off, len)); + AddressRange range = getAddressRange(); + if (!range.contains(addr)) { + throw new MemoryAccessException(); } + len = MathUtilities.unsignedMin(len, range.getMaxAddress().subtract(addr) + 1); + return program.memory.getBytes(addr, b, off, len); } @Override @@ -267,7 +232,6 @@ public abstract class AbstractDBTraceProgramViewMemoryBlock implements MemoryBlo @Override public boolean isOverlay() { - // TODO: What effect does this have? Does it makes sense for trace "overlays"? return getAddressSpace().isOverlaySpace(); } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewMemory.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewMemory.java index a7cfcdb076..240d88d8d8 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewMemory.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewMemory.java @@ -20,6 +20,7 @@ import java.util.function.Consumer; import java.util.function.Function; import ghidra.program.model.address.*; +import ghidra.program.model.mem.MemoryAccessException; import ghidra.program.model.mem.MemoryBlock; import ghidra.trace.model.memory.TraceMemoryRegion; import ghidra.util.LockHold; @@ -202,10 +203,6 @@ public class DBTraceProgramViewMemory extends AbstractDBTraceProgramViewMemory { if (regionBlocks == null) { // order return; } - for (AbstractDBTraceProgramViewMemoryBlock block : forceFullView - ? spaceBlocks.values() - : regionBlocks.values()) { - block.invalidateBytesCache(range); - } + cache.invalidate(range); } } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/CachePerDBTraceObject.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/CachePerDBTraceObject.java new file mode 100644 index 0000000000..a2d385e9e3 --- /dev/null +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/CachePerDBTraceObject.java @@ -0,0 +1,217 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.trace.database.target; + +import java.util.*; +import java.util.Map.Entry; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import ghidra.trace.model.Lifespan; + +public class CachePerDBTraceObject { + + private record SnapKey(long snap, String key) implements Comparable { + @Override + public int compareTo(SnapKey that) { + int c = Long.compare(this.snap, that.snap); + if (c != 0) { + return c; + } + if (this.key == that.key) { + return 0; + } + if (this.key == null) { + return 1; + } + if (that.key == null) { + return -1; + } + return this.key.compareTo(that.key); + } + + public static SnapKey forValue(DBTraceObjectValue value) { + return new SnapKey(value.getMinSnap(), value.getEntryKey()); + } + } + + public record Cached(boolean isMiss, T value) { + static final Cached MISS = new Cached<>(true, null); + + @SuppressWarnings("unchecked") + public static Cached miss() { + return (Cached) MISS; + } + + static Cached hit(T value) { + return new Cached<>(false, value); + } + } + + private static final int MAX_CACHE_KEYS = 200; + private static final int MAX_VALUES_PER_KEY = 20; + private static final int MAX_VALUES_ANY_KEY = 4000; + private static final int EXPANSION = 10; + + private record CachedLifespanValues(Lifespan span, + NavigableMap values) { + } + + private final Map> perKeyCache = new LinkedHashMap<>() { + protected boolean removeEldestEntry(Map.Entry> eldest) { + return size() > MAX_CACHE_KEYS; + } + }; + + private CachedLifespanValues anyKeyCache = null; + + private Stream doStreamAnyKey(NavigableMap map, + Lifespan lifespan) { + // TODO: Can be a HashMap, if that's faster + return map.values().stream().filter(v -> lifespan.intersects(v.getLifespan())); + } + + private Stream doStreamPerKey(NavigableMap map, + Lifespan lifespan, boolean forward) { + Long min = lifespan.min(); + var floor = map.floorEntry(min); + if (floor != null && floor.getValue().getLifespan().contains(min)) { + min = floor.getKey(); + } + NavigableMap sub = map.subMap(min, true, lifespan.max(), true); + if (forward) { + return sub.values().stream(); + } + return sub.descendingMap().values().stream(); + } + + private DBTraceObjectValue doGetValue(NavigableMap map, long snap) { + Entry floor = map.floorEntry(snap); + if (floor == null) { + return null; + } + DBTraceObjectValue value = floor.getValue(); + if (!value.getLifespan().contains(snap)) { + return null; + } + return value; + } + + public Cached> streamValues(Lifespan lifespan) { + if (anyKeyCache == null) { + return Cached.miss(); + } + if (!anyKeyCache.span.encloses(lifespan)) { + return Cached.miss(); + } + return Cached.hit(doStreamAnyKey(anyKeyCache.values, lifespan)); + } + + public Cached> streamValues(Lifespan lifespan, String key, + boolean forward) { + CachedLifespanValues cached = perKeyCache.get(key); + if (cached == null) { + return Cached.miss(); + } + if (!cached.span.encloses(lifespan)) { + return Cached.miss(); + } + return Cached.hit(doStreamPerKey(cached.values, lifespan, forward)); + } + + public Cached getValue(long snap, String key) { + CachedLifespanValues cached = perKeyCache.get(key); + if (cached == null) { + return Cached.miss(); + } + if (!cached.span.contains(snap)) { + return Cached.miss(); + } + return Cached.hit(doGetValue(cached.values, snap)); + } + + public Lifespan expandLifespan(Lifespan lifespan) { + // Expand the query to take advantage of spatial locality (in the time dimension) + long min = lifespan.lmin() - EXPANSION; + if (min > lifespan.lmin()) { + min = Lifespan.ALL.lmin(); + } + long max = lifespan.lmax() + EXPANSION; + if (max < lifespan.lmax()) { + max = Lifespan.ALL.lmax(); + } + return Lifespan.span(min, max); + } + + private DBTraceObjectValue mergeValues(DBTraceObjectValue v1, DBTraceObjectValue v2) { + throw new IllegalStateException("Conflicting values: %s, %s".formatted(v1, v2)); + } + + private NavigableMap collectAnyKey( + Stream values) { + return values.collect( + Collectors.toMap(SnapKey::forValue, v -> v, this::mergeValues, TreeMap::new)); + } + + private NavigableMap collectPerKey( + Stream values) { + return values.collect( + Collectors.toMap(v -> v.getLifespan().min(), v -> v, this::mergeValues, TreeMap::new)); + } + + public Stream offerStreamAnyKey(Lifespan expanded, + Stream values, Lifespan lifespan) { + NavigableMap map = collectAnyKey(values); + anyKeyCache = new CachedLifespanValues<>(expanded, map); + return doStreamAnyKey(map, lifespan); + } + + public Stream offerStreamPerKey(Lifespan expanded, + Stream values, Lifespan lifespan, String key, boolean forward) { + NavigableMap map = collectPerKey(values); + perKeyCache.put(key, new CachedLifespanValues<>(expanded, map)); + return doStreamPerKey(map, lifespan, forward); + } + + public DBTraceObjectValue offerGetValue(Lifespan expanded, Stream values, + long snap, String key) { + NavigableMap map = collectPerKey(values); + perKeyCache.put(key, new CachedLifespanValues<>(expanded, map)); + return doGetValue(map, snap); + } + + public void notifyValueCreated(DBTraceObjectValue value) { + Objects.requireNonNull(value); + if (anyKeyCache != null && anyKeyCache.span.intersects(value.getLifespan())) { + anyKeyCache.values.put(SnapKey.forValue(value), value); + } + CachedLifespanValues cached = perKeyCache.get(value.getEntryKey()); + if (cached != null && cached.span.intersects(value.getLifespan())) { + cached.values.put(value.getLifespan().min(), value); + } + } + + public void notifyValueDeleted(DBTraceObjectValue value) { + Objects.requireNonNull(value); + if (anyKeyCache != null) { + anyKeyCache.values.remove(SnapKey.forValue(value)); + } + CachedLifespanValues cached = perKeyCache.get(value.getEntryKey()); + if (cached != null) { + cached.values.remove(value.getLifespan().min()); + } + } +} diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObject.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObject.java index 070c03078a..82515aa92d 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObject.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObject.java @@ -35,7 +35,8 @@ import ghidra.trace.database.memory.DBTraceObjectRegister; import ghidra.trace.database.module.*; import ghidra.trace.database.stack.DBTraceObjectStack; import ghidra.trace.database.stack.DBTraceObjectStackFrame; -import ghidra.trace.database.target.InternalTraceObjectValue.ValueLifespanSetter; +import ghidra.trace.database.target.CachePerDBTraceObject.Cached; +import ghidra.trace.database.target.DBTraceObjectValue.ValueLifespanSetter; import ghidra.trace.database.target.ValueSpace.EntryKeyDimension; import ghidra.trace.database.target.ValueSpace.SnapDimension; import ghidra.trace.database.target.visitors.*; @@ -55,8 +56,7 @@ import ghidra.trace.model.target.annot.TraceObjectInterfaceUtils; import ghidra.trace.model.thread.TraceObjectThread; import ghidra.trace.util.TraceChangeRecord; import ghidra.trace.util.TraceEvents; -import ghidra.util.LockHold; -import ghidra.util.Msg; +import ghidra.util.*; import ghidra.util.database.*; import ghidra.util.database.DBCachedObjectStoreFactory.AbstractDBFieldCodec; import ghidra.util.database.annot.*; @@ -65,8 +65,6 @@ import ghidra.util.database.annot.*; public class DBTraceObject extends DBAnnotatedObject implements TraceObject { protected static final String TABLE_NAME = "Objects"; - private static final int VALUE_CACHE_SIZE = 50; - protected static // Map.Entry, Function> safeEntry( Class cls, Function ctor) { @@ -120,9 +118,6 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject { } } - record CachedLifespanValues(Lifespan span, Set values) { - } - // Canonical path static final String PATH_COLUMN_NAME = "Path"; @@ -140,17 +135,7 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject { private TargetObjectSchema targetSchema; private Map, TraceObjectInterface> ifaces; - private final Map valueCache = new LinkedHashMap<>() { - protected boolean removeEldestEntry(Map.Entry eldest) { - return size() > VALUE_CACHE_SIZE; - } - }; - private final Map nullCache = new LinkedHashMap<>() { - protected boolean removeEldestEntry(Map.Entry eldest) { - return size() > VALUE_CACHE_SIZE; - } - }; - private CachedLifespanValues cachedLifespanValues = null; + private final CachePerDBTraceObject cache = new CachePerDBTraceObject(); private volatile MutableLifeSet cachedLife = null; public DBTraceObject(DBTraceObjectManager manager, DBCachedObjectStore store, @@ -245,7 +230,7 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject { return DBTraceObjectValPath.of(); } DBTraceObject parent = doCreateCanonicalParentObject(); - InternalTraceObjectValue value = parent.setValue(lifespan, path.key(), this, resolution); + DBTraceObjectValue value = parent.setValue(lifespan, path.key(), this, resolution); // TODO: Should I re-order the recursion, so values are inserted from root to this? // TODO: Should child lifespans be allowed to exceed the parent's? DBTraceObjectValPath path = parent.doInsert(lifespan, resolution); @@ -276,10 +261,10 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject { } protected void doRemoveTree(Lifespan span) { - for (InternalTraceObjectValue parent : getParents(span)) { + for (DBTraceObjectValue parent : getParents(span)) { parent.doTruncateOrDeleteAndEmitLifeChange(span); } - for (InternalTraceObjectValue value : getValues(span)) { + for (DBTraceObjectValue value : getValues(span)) { value.doTruncateOrDeleteAndEmitLifeChange(span); if (value.isCanonical()) { value.getChild().doRemoveTree(span); @@ -294,28 +279,39 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject { } } + protected Stream streamCanonicalParentsData(Lifespan lifespan) { + return manager.valueMap.reduce(TraceObjectValueQuery.canonicalParents(this, lifespan)) + .values() + .stream(); + } + + protected Stream streamCanonicalParentsBehind(Lifespan lifespan) { + return manager.valueWbCache.streamCanonicalParents(this, lifespan); + } + + protected Stream streamCanonicalParents(Lifespan lifespan) { + return Stream.concat( + streamCanonicalParentsData(lifespan).map(v -> v.getWrapper()), + streamCanonicalParentsBehind(lifespan).map(v -> v.getWrapper())); + } + @Override public TraceObjectValue getCanonicalParent(long snap) { try (LockHold hold = manager.trace.lockRead()) { if (isRoot()) { return manager.getRootValue(); } - return manager.valueMap - .reduce(TraceObjectValueQuery.canonicalParents(this, Lifespan.at(snap))) - .firstValue(); + return streamCanonicalParents(Lifespan.at(snap)).findAny().orElse(null); } } @Override - public Stream getCanonicalParents(Lifespan lifespan) { + public Stream getCanonicalParents(Lifespan lifespan) { try (LockHold hold = manager.trace.lockRead()) { if (isRoot()) { return Stream.of(manager.getRootValue()); } - List list = List.copyOf( - manager.valueMap.reduce(TraceObjectValueQuery.canonicalParents(this, lifespan)) - .values()); - return list.stream(); + return streamCanonicalParents(lifespan).toList().stream(); } } @@ -350,56 +346,63 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject { return ifCls.cast(ifaces.get(ifCls)); } - protected Collection doGetParents(Lifespan lifespan) { - return List.copyOf( - manager.valueMap.reduce(TraceObjectValueQuery.parents(this, lifespan)).values()); + protected Stream streamParentsData(Lifespan lifespan) { + return manager.valueMap.reduce(TraceObjectValueQuery.parents(this, lifespan)) + .values() + .stream(); + } + + protected Stream streamParentsBehind(Lifespan lifespan) { + return manager.valueWbCache.streamParents(this, lifespan); + } + + protected Stream streamParents(Lifespan lifespan) { + return Stream.concat( + streamParentsData(lifespan).map(v -> v.getWrapper()), + streamParentsBehind(lifespan).map(v -> v.getWrapper())); } @Override - public Collection getParents(Lifespan lifespan) { + public Collection getParents(Lifespan lifespan) { try (LockHold hold = manager.trace.lockRead()) { - return doGetParents(lifespan); + return streamParents(lifespan).toList(); } } protected boolean doHasAnyValues() { - return !manager.valueMap.reduce(TraceObjectValueQuery.values(this, Lifespan.ALL)) - .isEmpty(); + return streamValuesW(Lifespan.ALL).findAny().isPresent(); } - protected Collection doGetValues(Lifespan lifespan) { + protected Stream streamValuesData(Lifespan lifespan) { return manager.valueMap .reduce(TraceObjectValueQuery.values(this, lifespan) .starting(EntryKeyDimension.FORWARD)) - .values(); + .values() + .stream(); } - protected Collection cachedDoGetValues(Lifespan lifespan) { - if (Long.compareUnsigned(lifespan.lmax() - lifespan.lmin(), 10) > 0) { - return List.copyOf(doGetValues(lifespan)); + protected Stream streamValuesBehind(Lifespan lifespan) { + return manager.valueWbCache.streamValues(this, lifespan); + } + + protected Stream streamValuesW(Lifespan lifespan) { + return Stream.concat( + streamValuesData(lifespan).map(d -> d.getWrapper()), + streamValuesBehind(lifespan).map(b -> b.getWrapper())); + } + + protected Stream streamValuesR(Lifespan lifespan) { + Cached> cached = cache.streamValues(lifespan); + if (!cached.isMiss()) { + return cached.value(); } - if (cachedLifespanValues == null || !cachedLifespanValues.span.encloses(lifespan)) { - // Expand the query to take advantage of spatial locality (in the time dimension) - long min = lifespan.lmin() - 10; - if (min > lifespan.lmin()) { - min = Lifespan.ALL.lmin(); - } - long max = lifespan.lmax() + 10; - if (max < lifespan.lmax()) { - max = Lifespan.ALL.lmax(); - } - Lifespan expanded = Lifespan.span(min, max); - cachedLifespanValues = - new CachedLifespanValues(expanded, new HashSet<>(doGetValues(expanded))); - } - return cachedLifespanValues.values.stream() - .filter(v -> v.getLifespan().intersects(lifespan)) - .toList(); + Lifespan expanded = cache.expandLifespan(lifespan); + Stream stream = streamValuesW(expanded); + return cache.offerStreamAnyKey(expanded, stream, lifespan); } protected boolean doHasAnyParents() { - return !manager.valueMap.reduce(TraceObjectValueQuery.parents(this, Lifespan.ALL)) - .isEmpty(); + return streamParents(Lifespan.ALL).findAny().isPresent(); } protected boolean doIsConnected() { @@ -407,28 +410,32 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject { } @Override - public Collection getValues(Lifespan lifespan) { + public Collection getValues(Lifespan lifespan) { try (LockHold hold = manager.trace.lockRead()) { - return cachedDoGetValues(lifespan); + return streamValuesR(lifespan).toList(); } } @Override - public Collection getElements(Lifespan lifespan) { - return getValues(lifespan).stream() - .filter(v -> PathUtils.isIndex(v.getEntryKey())) - .toList(); + public Collection getElements(Lifespan lifespan) { + try (LockHold hold = manager.trace.lockRead()) { + return streamValuesR(lifespan) + .filter(v -> PathUtils.isIndex(v.getEntryKey())) + .toList(); + } } @Override - public Collection getAttributes(Lifespan lifespan) { - return getValues(lifespan).stream() - .filter(v -> PathUtils.isName(v.getEntryKey())) - .toList(); + public Collection getAttributes(Lifespan lifespan) { + try (LockHold hold = manager.trace.lockRead()) { + return streamValuesR(lifespan) + .filter(v -> PathUtils.isName(v.getEntryKey())) + .toList(); + } } protected void doCheckConflicts(Lifespan span, String key, Object value) { - for (InternalTraceObjectValue val : doGetValues(span, key, true)) { + for (DBTraceObjectValue val : StreamUtils.iter(streamValuesR(span, key, true))) { if (!Objects.equals(value, val.getValue())) { throw new DuplicateKeyException(key); } @@ -438,7 +445,7 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject { protected Lifespan doAdjust(Lifespan span, String key, Object value) { // Ordered by min, so I only need to consider the first conflict // If start is contained in an entry, assume the user means to overwrite it. - for (InternalTraceObjectValue val : doGetValues(span, key, true)) { + for (DBTraceObjectValue val : StreamUtils.iter(streamValuesR(span, key, true))) { if (Objects.equals(value, val.getValue())) { continue; // not a conflict } @@ -451,63 +458,94 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject { return span; } - protected Collection doGetValues(Lifespan span, - String key, boolean forward) { + protected Stream streamValuesData(Lifespan span, String key, + boolean forward) { return manager.valueMap .reduce(TraceObjectValueQuery.values(this, key, key, span) .starting(forward ? SnapDimension.FORWARD : SnapDimension.BACKWARD)) - .orderedValues(); + .orderedValues() + .stream(); + } + + protected Stream streamValuesBehind(Lifespan span, String key, + boolean forward) { + return manager.valueWbCache.streamValues(this, key, span, forward); + } + + protected Stream streamValuesW(Lifespan span, String key, boolean forward) { + return StreamUtils.merge(List.of( + streamValuesData(span, key, forward).map(d -> d.getWrapper()), + streamValuesBehind(span, key, forward).map(b -> b.getWrapper())), + Comparator.comparing(forward ? v -> v.getMinSnap() : v -> -v.getMaxSnap())); + } + + protected Stream streamValuesR(Lifespan span, String key, boolean forward) { + Cached> cached = cache.streamValues(span, key, forward); + if (!cached.isMiss()) { + return cached.value(); + } + Lifespan expanded = cache.expandLifespan(span); + Stream stream = streamValuesW(expanded, key, forward); + return cache.offerStreamPerKey(expanded, stream, span, key, forward); } @Override - public Collection getValues(Lifespan span, String key) { + public Collection getValues(Lifespan span, String key) { try (LockHold hold = manager.trace.lockRead()) { String k = getTargetSchema().checkAliasedAttribute(key); - return doGetValues(span, k, true); + return streamValuesR(span, k, true).toList(); + } + } + + protected DBTraceObjectValue getValueW(long snap, String key) { + DBTraceObjectValueBehind behind = manager.valueWbCache.get(this, key, snap); + if (behind != null) { + return behind.getWrapper(); + } + DBTraceObjectValueData data = manager.valueMap + .reduce(TraceObjectValueQuery.values(this, key, key, Lifespan.at(snap))) + .firstValue(); + if (data != null) { + return data.getWrapper(); + } + return null; + } + + protected DBTraceObjectValue getValueR(long snap, String key) { + Cached cached = cache.getValue(snap, key); + if (!cached.isMiss()) { + return cached.value(); + } + Lifespan expanded = cache.expandLifespan(Lifespan.at(snap)); + Stream stream = streamValuesW(expanded, key, true); + return cache.offerGetValue(expanded, stream, snap, key); + } + + @Override + public DBTraceObjectValue getValue(long snap, String key) { + try (LockHold hold = manager.trace.lockRead()) { + String k = getTargetSchema().checkAliasedAttribute(key); + return getValueR(snap, k); } } @Override - public InternalTraceObjectValue getValue(long snap, String key) { - try (LockHold hold = manager.trace.lockRead()) { - String k = getTargetSchema().checkAliasedAttribute(key); - InternalTraceObjectValue cached = valueCache.get(k); - if (cached != null && !cached.isDeleted() && cached.getLifespan().contains(snap)) { - return cached; - } - Long nullSnap = nullCache.get(k); - if (nullSnap != null && nullSnap.longValue() == snap) { - return null; - } - InternalTraceObjectValue found = manager.valueMap - .reduce(TraceObjectValueQuery.values(this, k, k, Lifespan.at(snap))) - .firstValue(); - if (found == null) { - nullCache.put(k, snap); - } - else { - valueCache.put(k, found); - } - return found; - } - } - - @Override - public Stream getOrderedValues(Lifespan span, String key, + public Stream getOrderedValues(Lifespan span, String key, boolean forward) { try (LockHold hold = manager.trace.lockRead()) { String k = getTargetSchema().checkAliasedAttribute(key); - return doGetValues(span, k, forward).stream(); + // Locking issue if we stream lazily. Capture to list with lock + return streamValuesR(span, k, forward).toList().stream(); } } @Override - public InternalTraceObjectValue getElement(long snap, String index) { + public DBTraceObjectValue getElement(long snap, String index) { return getValue(snap, PathUtils.makeKey(index)); } @Override - public InternalTraceObjectValue getElement(long snap, long index) { + public DBTraceObjectValue getElement(long snap, long index) { return getElement(snap, PathUtils.makeIndex(index)); } @@ -521,8 +559,9 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject { protected Stream doStreamVisitor(Lifespan span, Visitor visitor) { + // Capturing to list with lock return TreeTraversal.INSTANCE.walkObject(visitor, this, span, - DBTraceObjectValPath.of()); + DBTraceObjectValPath.of()).toList().stream(); } @Override @@ -566,7 +605,8 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject { DBTraceObjectValPath empty = DBTraceObjectValPath.of(); try (LockHold hold = manager.trace.lockRead()) { if (relativePath.isRoot()) { - return Stream.of(empty); // Not the empty stream + // Singleton of empty path (not the empty stream) + return Stream.of(empty); } return doStreamVisitor(span, new OrderedSuccessorsVisitor(relativePath, forward)); @@ -587,17 +627,12 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject { } } - protected InternalTraceObjectValue doCreateValue(Lifespan lifespan, String key, - Object value) { - Long nullSnap = nullCache.get(key); - if (nullSnap != null && lifespan.contains(nullSnap)) { - nullCache.remove(key); - } + protected DBTraceObjectValue doCreateValue(Lifespan lifespan, String key, Object value) { return manager.doCreateValue(lifespan, this, key, value); } @Override - public InternalTraceObjectValue setValue(Lifespan lifespan, String key, Object value, + public DBTraceObjectValue setValue(Lifespan lifespan, String key, Object value, ConflictResolution resolution) { try (LockHold hold = manager.trace.lockWrite()) { if (isDeleted()) { @@ -614,14 +649,13 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject { DBTraceObject canonicalLifeChanged = null; @Override - protected Iterable getIntersecting(Long lower, + protected Iterable getIntersecting(Long lower, Long upper) { - return Collections.unmodifiableCollection( - doGetValues(Lifespan.span(lower, upper), k, true)); + return StreamUtils.iter(streamValuesR(Lifespan.span(lower, upper), k, true)); } @Override - protected void remove(InternalTraceObjectValue entry) { + protected void remove(DBTraceObjectValue entry) { if (entry.isCanonical()) { canonicalLifeChanged = entry.getChild(); } @@ -629,8 +663,8 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject { } @Override - protected InternalTraceObjectValue put(Lifespan range, Object value) { - InternalTraceObjectValue entry = super.put(range, value); + protected DBTraceObjectValue put(Lifespan range, Object value) { + DBTraceObjectValue entry = super.put(range, value); if (entry != null && entry.isCanonical()) { canonicalLifeChanged = entry.getChild(); } @@ -638,11 +672,11 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject { } @Override - protected InternalTraceObjectValue create(Lifespan range, Object value) { + protected DBTraceObjectValue create(Lifespan range, Object value) { return doCreateValue(range, k, value); } }; - InternalTraceObjectValue result = setter.set(lifespan, value); + DBTraceObjectValue result = setter.set(lifespan, value); DBTraceObject child = setter.canonicalLifeChanged; if (child != null) { @@ -767,10 +801,10 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject { } protected void doDeleteReferringValues() { - for (InternalTraceObjectValue child : getValues(Lifespan.ALL)) { + for (DBTraceObjectValue child : getValues(Lifespan.ALL)) { child.doDeleteAndEmit(); } - for (InternalTraceObjectValue parent : getParents(Lifespan.ALL)) { + for (DBTraceObjectValue parent : getParents(Lifespan.ALL)) { parent.doDeleteAndEmit(); } } @@ -800,21 +834,16 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject { } } - protected void notifyValueCreated(InternalTraceObjectValue value) { - if (cachedLifespanValues != null) { - if (cachedLifespanValues.span.intersects(value.getLifespan())) { - cachedLifespanValues.values.add(value); - } - } + protected void notifyValueCreated(DBTraceObjectValue value) { + cache.notifyValueCreated(value); } - protected void notifyValueDeleted(InternalTraceObjectValue value) { - if (cachedLifespanValues != null) { - cachedLifespanValues.values.remove(value); - } + protected void notifyValueDeleted(DBTraceObjectValue value) { + cache.notifyValueDeleted(value); } - protected void notifyParentValueCreated(InternalTraceObjectValue parent) { + protected void notifyParentValueCreated(DBTraceObjectValue parent) { + Objects.requireNonNull(parent); if (cachedLife != null && parent.isCanonical()) { synchronized (cachedLife) { cachedLife.add(parent.getLifespan()); @@ -822,7 +851,8 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject { } } - protected void notifyParentValueDeleted(InternalTraceObjectValue parent) { + protected void notifyParentValueDeleted(DBTraceObjectValue parent) { + Objects.requireNonNull(parent); if (cachedLife != null && parent.isCanonical()) { synchronized (cachedLife) { cachedLife.remove(parent.getLifespan()); diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectDBFieldCodec.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectDBFieldCodec.java index 8a5526f9ea..c90726329b 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectDBFieldCodec.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectDBFieldCodec.java @@ -22,7 +22,7 @@ import db.LongField; import ghidra.util.database.DBAnnotatedObject; import ghidra.util.database.DBCachedObjectStoreFactory.AbstractDBFieldCodec; -public class DBTraceObjectDBFieldCodec +public class DBTraceObjectDBFieldCodec extends AbstractDBFieldCodec { public DBTraceObjectDBFieldCodec(Class objectType, Field field, int column) { super(DBTraceObject.class, objectType, LongField.class, field, column); @@ -32,7 +32,7 @@ public class DBTraceObjectDBFieldCodec objectStore; protected final DBTraceObjectValueRStarTree valueTree; protected final DBTraceObjectValueMap valueMap; + protected final DBTraceObjectValueWriteBehindCache valueWbCache; protected final DBCachedObjectIndex objectsByPath; protected final Collection objectsView; - protected final Collection valuesView; protected TargetObjectSchema rootSchema; @@ -198,8 +196,9 @@ public class DBTraceObjectManager implements TraceObjectManager, DBTraceManager objectsByPath = objectStore.getIndex(TraceObjectKeyPath.class, DBTraceObject.PATH_COLUMN); + valueWbCache = new DBTraceObjectValueWriteBehindCache(this); + objectsView = Collections.unmodifiableCollection(objectStore.asMap().values()); - valuesView = Collections.unmodifiableCollection(valueMap.values()); } protected void loadRootSchema() { @@ -228,29 +227,42 @@ public class DBTraceObjectManager implements TraceObjectManager, DBTraceManager schemasByInterface.clear(); } - @Internal + protected boolean checkMyObject(DBTraceObject object) { + if (object.manager != this) { + return false; + } + if (!objectStore.asMap().values().contains(object)) { + return false; + } + return true; + } + protected DBTraceObject assertIsMine(TraceObject object) { - if (!(object instanceof DBTraceObject)) { + if (!(object instanceof DBTraceObject dbObject)) { throw new IllegalArgumentException("Object " + object + " is not part of this trace"); } - DBTraceObject dbObject = (DBTraceObject) object; - if (dbObject.manager != this) { - throw new IllegalArgumentException("Object " + object + " is not part of this trace"); - } - if (!getAllObjects().contains(dbObject)) { + if (!checkMyObject(dbObject)) { throw new IllegalArgumentException("Object " + object + " is not part of this trace"); } return dbObject; } - protected Object validatePrimitive(Object child) { + protected Object validatePrimitive(Object value) { try { - PrimitiveCodec.getCodec(child.getClass()); + PrimitiveCodec.getCodec(value.getClass()); } catch (IllegalArgumentException e) { - throw new IllegalArgumentException("Cannot encode " + child, e); + throw new IllegalArgumentException("Cannot encode " + value, e); } - return child; + return value; + } + + protected Object validateValue(Object value) { + if (value instanceof TraceObject | value instanceof Address | + value instanceof AddressRange) { + return value; + } + return validatePrimitive(value); } @Override @@ -267,7 +279,7 @@ public class DBTraceObjectManager implements TraceObjectManager, DBTraceManager rootSchema = schema; } - protected void emitValueCreated(DBTraceObject parent, InternalTraceObjectValue entry) { + protected void emitValueCreated(DBTraceObject parent, DBTraceObjectValue entry) { if (parent == null) { // Don't need event for root value created return; @@ -275,24 +287,30 @@ public class DBTraceObjectManager implements TraceObjectManager, DBTraceManager parent.emitEvents(new TraceChangeRecord<>(TraceEvents.VALUE_CREATED, null, entry)); } - protected InternalTraceObjectValue doCreateValue(Lifespan lifespan, + protected DBTraceObjectValueData doCreateValueData(Lifespan lifespan, DBTraceObject parent, + String key, Object value) { + DBTraceObjectValueData entry = + valueMap.put(new ImmutableValueShape(parent, value, key, lifespan), null); + if (!(value instanceof DBTraceObject)) { + entry.doSetPrimitive(value); + } + return entry; + } + + protected DBTraceObjectValue doCreateValue(Lifespan lifespan, DBTraceObject parent, String key, Object value) { - InternalTraceObjectValue entry = valueTree.asSpatialMap() - .put(new ImmutableValueShape(parent, value, key, lifespan), null); + // Root is never in write-behind cache + DBTraceObjectValue entry = parent == null + ? doCreateValueData(lifespan, parent, key, value).getWrapper() + : valueWbCache.doCreateValue(lifespan, parent, key, value).getWrapper(); + if (parent != null) { + parent.notifyValueCreated(entry); + } if (value instanceof DBTraceObject child) { child.notifyParentValueCreated(entry); } - else { - entry.doSetPrimitive(value); - } - - if (parent != null) { // Root - parent.notifyValueCreated(entry); - } - // TODO: Perhaps a little drastic invalidateObjectsContainingCache(); - emitValueCreated(parent, entry); return entry; } @@ -326,13 +344,13 @@ public class DBTraceObjectManager implements TraceObjectManager, DBTraceManager } @Override - public TraceObjectValue createRootObject(TargetObjectSchema schema) { + public DBTraceObjectValue createRootObject(TargetObjectSchema schema) { try (LockHold hold = trace.lockWrite()) { setSchema(schema); DBTraceObject root = doCreateObject(TraceObjectKeyPath.of()); assert root.getKey() == 0; - InternalTraceObjectValue val = doCreateValue(Lifespan.ALL, null, "", root); - assert val.getKey() == 0; + DBTraceObjectValue val = doCreateValue(Lifespan.ALL, null, "", root); + assert val.getWrapped() instanceof DBTraceObjectValueData data && data.getKey() == 0; return val; } } @@ -344,9 +362,10 @@ public class DBTraceObjectManager implements TraceObjectManager, DBTraceManager } } - public DBTraceObjectValueData getRootValue() { + public DBTraceObjectValue getRootValue() { try (LockHold hold = trace.lockRead()) { - return valueTree.getDataStore().getObjectAt(0); + DBTraceObjectValueData data = valueTree.getDataStore().getObjectAt(0); + return data == null ? null : data.getWrapper(); } } @@ -381,7 +400,7 @@ public class DBTraceObjectManager implements TraceObjectManager, DBTraceManager public Stream getValuePaths(Lifespan span, PathPredicates predicates) { try (LockHold hold = trace.lockRead()) { - DBTraceObjectValueData rootVal = getRootValue(); + DBTraceObjectValue rootVal = getRootValue(); if (rootVal == null) { return Stream.of(); } @@ -390,29 +409,60 @@ public class DBTraceObjectManager implements TraceObjectManager, DBTraceManager } @Override - public Collection getAllObjects() { - return objectsView; + public Stream getAllObjects() { + return objectStore.asMap().values().stream(); } @Override - public Collection getAllValues() { - return valuesView; + public int getObjectCount() { + return objectStore.getRecordCount(); + } + + @Override + public Stream getAllValues() { + return Stream.concat( + valueMap.values().stream().map(v -> v.getWrapper()), + valueWbCache.streamAllValues().map(v -> v.getWrapper())); + } + + protected Stream streamValuesIntersectingData(Lifespan span, + AddressRange range, String entryKey) { + return valueMap.reduce(TraceObjectValueQuery.intersecting( + entryKey != null ? entryKey : EntryKeyDimension.INSTANCE.absoluteMin(), + entryKey != null ? entryKey : EntryKeyDimension.INSTANCE.absoluteMax(), + span, range)).values().stream(); + } + + protected Stream streamValuesIntersectingBehind(Lifespan span, + AddressRange range, String entryKey) { + return valueWbCache.streamValuesIntersecting(span, range, entryKey); } @Override public Collection getValuesIntersecting(Lifespan span, AddressRange range, String entryKey) { - return Collections - .unmodifiableCollection(valueMap.reduce(TraceObjectValueQuery.intersecting( - entryKey != null ? entryKey : EntryKeyDimension.INSTANCE.absoluteMin(), - entryKey != null ? entryKey : EntryKeyDimension.INSTANCE.absoluteMax(), - span, range)).values()); + return Stream.concat( + streamValuesIntersectingData(span, range, entryKey).map(v -> v.getWrapper()), + streamValuesIntersectingBehind(span, range, entryKey).map(v -> v.getWrapper())) + .toList(); + } + + protected Stream streamValuesAtData(long snap, Address address, + String entryKey) { + return valueMap.reduce(TraceObjectValueQuery.at(entryKey, snap, address)).values().stream(); + } + + protected Stream streamValuesAtBehind(long snap, Address address, + String entryKey) { + return valueWbCache.streamValuesAt(snap, address, entryKey); } public Collection getValuesAt(long snap, Address address, String entryKey) { - return Collections.unmodifiableCollection( - valueMap.reduce(TraceObjectValueQuery.at(entryKey, snap, address)).values()); + return Stream.concat( + streamValuesAtData(snap, address, entryKey).map(v -> v.getWrapper()), + streamValuesAtBehind(snap, address, entryKey).map(v -> v.getWrapper())) + .toList(); } @Override @@ -450,6 +500,7 @@ public class DBTraceObjectManager implements TraceObjectManager, DBTraceManager public void clear() { try (LockHold hold = trace.lockWrite()) { valueMap.clear(); + valueWbCache.clear(); objectStore.deleteAll(); schemaStore.deleteAll(); rootSchema = null; @@ -463,8 +514,8 @@ public class DBTraceObjectManager implements TraceObjectManager, DBTraceManager object.emitEvents(new TraceChangeRecord<>(TraceEvents.OBJECT_DELETED, null, object)); } - protected void doDeleteEdge(DBTraceObjectValueData edge) { - valueTree.doDeleteEntry(edge); + protected void doDeleteValue(DBTraceObjectValueData value) { + valueTree.doDeleteEntry(value); // TODO: Perhaps a little drastic.... /** @@ -475,6 +526,12 @@ public class DBTraceObjectManager implements TraceObjectManager, DBTraceManager invalidateObjectsContainingCache(); } + protected void doDeleteCachedValue(DBTraceObjectValueBehind value) { + valueWbCache.remove(value); + // Ditto NB from doDeleteValue + invalidateObjectsContainingCache(); + } + public boolean hasSchema() { return rootSchema != null; } @@ -594,22 +651,28 @@ public class DBTraceObjectManager implements TraceObjectManager, DBTraceManager } } + static boolean acceptValue(DBTraceObjectValue value, + String key, Class ifaceCls, Predicate predicate) { + if (!value.hasEntryKey(key)) { + return false; + } + TraceObject parent = value.getParent(); + I iface = parent.queryInterface(ifaceCls); + if (iface == null) { + return false; + } + if (!predicate.test(iface)) { + return false; + } + return true; + } + public AddressSetView getObjectsAddressSet(long snap, String key, Class ifaceCls, Predicate predicate) { - return valueMap.getAddressSetView(Lifespan.at(snap), v -> { - if (!v.hasEntryKey(key)) { - return false; - } - TraceObject parent = v.getParent(); - I iface = parent.queryInterface(ifaceCls); - if (iface == null) { - return false; - } - if (!predicate.test(iface)) { - return false; - } - return true; - }); + return new UnionAddressSetView( + valueMap.getAddressSetView(Lifespan.at(snap), + v -> acceptValue(v.getWrapper(), key, ifaceCls, predicate)), + valueWbCache.getObjectsAddresSet(snap, key, ifaceCls, predicate)); } public I getSuccessor(TraceObject seed, @@ -751,24 +814,21 @@ public class DBTraceObjectManager implements TraceObjectManager, DBTraceManager } } - public boolean checkMyObject(DBTraceObject object) { - if (object.manager != this) { - return false; - } - if (!getAllObjects().contains(object)) { - return false; - } - return true; - } - public TraceThread assertMyThread(TraceThread thread) { - if (!(thread instanceof DBTraceObjectThread)) { + if (!(thread instanceof DBTraceObjectThread dbThread)) { throw new AssertionError("Thread " + thread + " is not an object in this trace"); } - DBTraceObjectThread dbThread = (DBTraceObjectThread) thread; if (!checkMyObject(dbThread.getObject())) { throw new AssertionError("Thread " + thread + " is not an object in this trace"); } return dbThread; } + + public void flushWbCaches() { + valueWbCache.flush(); + } + + public void waitWbWorkers() { + valueWbCache.waitWorkers(); + } } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectValPath.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectValPath.java index e1b45389a6..215b429a96 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectValPath.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectValPath.java @@ -28,18 +28,18 @@ public class DBTraceObjectValPath implements TraceObjectValPath { return EMPTY; } - public static DBTraceObjectValPath of(Collection entryList) { + public static DBTraceObjectValPath of(Collection entryList) { return new DBTraceObjectValPath(List.copyOf(entryList)); } - public static DBTraceObjectValPath of(InternalTraceObjectValue... entries) { + public static DBTraceObjectValPath of(DBTraceObjectValue... entries) { return DBTraceObjectValPath.of(Arrays.asList(entries)); } - private final List entryList; + private final List entryList; private List keyList; // lazily computed - private DBTraceObjectValPath(List entryList) { + private DBTraceObjectValPath(List entryList) { this.entryList = entryList; } @@ -49,7 +49,7 @@ public class DBTraceObjectValPath implements TraceObjectValPath { } @Override - public List getEntryList() { + public List getEntryList() { return entryList; } @@ -77,10 +77,10 @@ public class DBTraceObjectValPath implements TraceObjectValPath { if (!entryList.isEmpty() && entry.getTrace() != entryList.get(0).getTrace()) { throw new IllegalArgumentException("All values in path must be from the same trace"); } - if (!(entry instanceof InternalTraceObjectValue val)) { + if (!(entry instanceof DBTraceObjectValue val)) { throw new IllegalArgumentException("Value must be in the database"); } - InternalTraceObjectValue[] arr = new InternalTraceObjectValue[1 + entryList.size()]; + DBTraceObjectValue[] arr = new DBTraceObjectValue[1 + entryList.size()]; arr[0] = val; for (int i = 1; i < arr.length; i++) { arr[i] = entryList.get(i - 1); @@ -93,10 +93,10 @@ public class DBTraceObjectValPath implements TraceObjectValPath { if (!entryList.isEmpty() && entry.getTrace() != entryList.get(0).getTrace()) { throw new IllegalArgumentException("All values in path must be from the same trace"); } - if (!(entry instanceof InternalTraceObjectValue val)) { + if (!(entry instanceof DBTraceObjectValue val)) { throw new IllegalArgumentException("Value must be in the database"); } - InternalTraceObjectValue[] arr = new InternalTraceObjectValue[1 + entryList.size()]; + DBTraceObjectValue[] arr = new DBTraceObjectValue[1 + entryList.size()]; for (int i = 0; i < arr.length - 1; i++) { arr[i] = entryList.get(i); } @@ -105,7 +105,7 @@ public class DBTraceObjectValPath implements TraceObjectValPath { } @Override - public InternalTraceObjectValue getFirstEntry() { + public DBTraceObjectValue getFirstEntry() { if (entryList.isEmpty()) { return null; } @@ -114,12 +114,12 @@ public class DBTraceObjectValPath implements TraceObjectValPath { @Override public TraceObject getSource(TraceObject ifEmpty) { - InternalTraceObjectValue first = getFirstEntry(); + DBTraceObjectValue first = getFirstEntry(); return first == null ? ifEmpty : first.getParent(); } @Override - public InternalTraceObjectValue getLastEntry() { + public DBTraceObjectValue getLastEntry() { if (entryList.isEmpty()) { return null; } @@ -128,13 +128,13 @@ public class DBTraceObjectValPath implements TraceObjectValPath { @Override public Object getDestinationValue(Object ifEmpty) { - InternalTraceObjectValue last = getLastEntry(); + DBTraceObjectValue last = getLastEntry(); return last == null ? ifEmpty : last.getValue(); } @Override public TraceObject getDestination(TraceObject ifEmpty) { - InternalTraceObjectValue last = getLastEntry(); + DBTraceObjectValue last = getLastEntry(); return last == null ? ifEmpty : last.getChild(); } } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectValue.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectValue.java new file mode 100644 index 0000000000..61dbf11050 --- /dev/null +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectValue.java @@ -0,0 +1,400 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.trace.database.target; + +import java.util.*; +import java.util.stream.Stream; + +import ghidra.trace.database.DBTraceUtils.LifespanMapSetter; +import ghidra.trace.database.target.visitors.TreeTraversal; +import ghidra.trace.database.target.visitors.TreeTraversal.Visitor; +import ghidra.trace.model.Lifespan; +import ghidra.trace.model.Trace; +import ghidra.trace.model.target.*; +import ghidra.trace.model.target.TraceObject.ConflictResolution; +import ghidra.trace.util.TraceChangeRecord; +import ghidra.trace.util.TraceEvents; +import ghidra.util.LockHold; +import ghidra.util.StreamUtils; + +public class DBTraceObjectValue implements TraceObjectValue { + + static abstract class ValueLifespanSetter + extends LifespanMapSetter { + protected final Lifespan range; + protected final Object value; + protected DBTraceObjectValue keep = null; + protected Collection kept = new ArrayList<>(2); + + public ValueLifespanSetter(Lifespan range, Object value) { + this.range = range; + this.value = value; + } + + public ValueLifespanSetter(Lifespan range, Object value, + DBTraceObjectValue keep) { + this(range, value); + this.keep = keep; + } + + @Override + protected Lifespan getRange(DBTraceObjectValue entry) { + return entry.getLifespan(); + } + + @Override + protected Object getValue(DBTraceObjectValue entry) { + return entry.getValue(); + } + + @Override + protected boolean valuesEqual(Object v1, Object v2) { + if (Objects.equals(v1, v2)) { + return true; + } + if (v1 == null || !v1.getClass().isArray()) { + return false; + } + if (v1 instanceof boolean[] a1 && v2 instanceof boolean[] a2) { + return Arrays.equals(a1, a2); + } + if (v1 instanceof byte[] a1 && v2 instanceof byte[] a2) { + return Arrays.equals(a1, a2); + } + if (v1 instanceof char[] a1 && v2 instanceof char[] a2) { + return Arrays.equals(a1, a2); + } + if (v1 instanceof double[] a1 && v2 instanceof double[] a2) { + return Arrays.equals(a1, a2); + } + if (v1 instanceof float[] a1 && v2 instanceof float[] a2) { + return Arrays.equals(a1, a2); + } + if (v1 instanceof int[] a1 && v2 instanceof int[] a2) { + return Arrays.equals(a1, a2); + } + if (v1 instanceof long[] a1 && v2 instanceof long[] a2) { + return Arrays.equals(a1, a2); + } + if (v1 instanceof short[] a1 && v2 instanceof short[] a2) { + return Arrays.equals(a1, a2); + } + return false; + } + + @Override + protected void remove(DBTraceObjectValue entry) { + if (valuesEqual(entry.getValue(), value)) { + if (keep == null) { + keep = entry; + } + else { + entry.doDeleteAndEmit(); + } + } + else { + DBTraceObjectValue created = entry.doTruncateOrDelete(range); + if (!entry.isDeleted()) { + kept.add(entry); + } + if (created != null) { + kept.add(created); + } + } + } + + @Override + protected DBTraceObjectValue put(Lifespan range, Object value) { + if (value == null) { + return null; + } + if (keep != null && valuesEqual(this.value, value)) { + keep.doSetLifespanAndEmit(range); + return keep; + } + for (DBTraceObjectValue k : kept) { + if (valuesEqual(value, k.getValue()) && Objects.equals(range, k.getLifespan())) { + kept.remove(k); + return k; + } + } + return create(range, value); + } + + protected abstract DBTraceObjectValue create(Lifespan range, Object value); + } + + private final DBTraceObjectManager manager; + + private volatile TraceObjectValueStorage wrapped; + + public DBTraceObjectValue(DBTraceObjectManager manager, + TraceObjectValueStorage wrapped) { + this.manager = manager; + this.wrapped = wrapped; + } + + void setWrapped(TraceObjectValueStorage wrapped) { + this.wrapped = wrapped; + if (wrapped instanceof DBTraceObjectValueData data) { + data.setWrapper(this); + } + } + + void doSetLifespanAndEmit(Lifespan lifespan) { + Lifespan oldLifespan = getLifespan(); + doSetLifespan(lifespan); + getParent().emitEvents(new TraceChangeRecord<>(TraceEvents.VALUE_LIFESPAN_CHANGED, + null, this, oldLifespan, lifespan)); + } + + @Override + public Trace getTrace() { + return manager.trace; + } + + @Override + public String getEntryKey() { + try (LockHold hold = LockHold.lock(manager.lock.readLock())) { + return wrapped.getEntryKey(); + } + } + + protected TraceObjectKeyPath doGetCanonicalPath() { + DBTraceObject parent = wrapped.getParent(); + if (parent == null) { + return TraceObjectKeyPath.of(); + } + return parent.getCanonicalPath().extend(wrapped.getEntryKey()); + } + + @Override + public TraceObjectKeyPath getCanonicalPath() { + try (LockHold hold = LockHold.lock(manager.lock.readLock())) { + return doGetCanonicalPath(); + } + } + + @Override + public Object getValue() { + try (LockHold hold = LockHold.lock(manager.lock.readLock())) { + return wrapped.getValue(); + } + } + + @Override + public boolean isObject() { + try (LockHold hold = LockHold.lock(manager.lock.readLock())) { + return wrapped.getChildOrNull() != null; + } + } + + protected boolean doIsCanonical() { + DBTraceObject child = wrapped.getChildOrNull(); + if (child == null) { + return false; + } + if (wrapped.getParent() == null) { // We're the root + return true; + } + return doGetCanonicalPath().equals(child.getCanonicalPath()); + } + + @Override + public boolean isCanonical() { + try (LockHold hold = LockHold.lock(manager.lock.readLock())) { + return doIsCanonical(); + } + } + + @Override + public Lifespan getLifespan() { + try (LockHold hold = LockHold.lock(manager.lock.readLock())) { + return wrapped.getLifespan(); + } + } + + @Override + public void setMinSnap(long minSnap) { + try (LockHold hold = LockHold.lock(manager.lock.writeLock())) { + setLifespan(Lifespan.span(minSnap, getLifespan().lmax())); + } + } + + @Override + public long getMinSnap() { + try (LockHold hold = LockHold.lock(manager.lock.readLock())) { + return wrapped.getLifespan().lmin(); + } + } + + @Override + public void setMaxSnap(long maxSnap) { + try (LockHold hold = LockHold.lock(manager.lock.writeLock())) { + setLifespan(Lifespan.span(getLifespan().lmin(), maxSnap)); + } + } + + @Override + public long getMaxSnap() { + try (LockHold hold = LockHold.lock(manager.lock.readLock())) { + return wrapped.getLifespan().lmax(); + } + } + + void doDelete() { + getParent().notifyValueDeleted(this); + DBTraceObject child = wrapped.getChildOrNull(); + if (child != null) { + child.notifyParentValueDeleted(this); + } + wrapped.doDelete(); + } + + void doDeleteAndEmit() { + DBTraceObject parent = getParent(); + doDelete(); + parent.emitEvents(new TraceChangeRecord<>(TraceEvents.VALUE_DELETED, null, this)); + } + + @Override + public void delete() { + try (LockHold hold = LockHold.lock(manager.lock.writeLock())) { + if (getParent() == null) { + throw new IllegalArgumentException("Cannot delete root value"); + } + doDeleteAndEmit(); + } + } + + @Override + public boolean isDeleted() { + try (LockHold hold = LockHold.lock(manager.lock.readLock())) { + return wrapped.isDeleted(); + } + } + + @Override + public DBTraceObjectValue truncateOrDelete(Lifespan span) { + try (LockHold hold = LockHold.lock(manager.lock.writeLock())) { + if (wrapped.getParent() == null) { + throw new IllegalArgumentException("Cannot truncate or delete root value"); + } + return doTruncateOrDeleteAndEmitLifeChange(span); + } + } + + @Override + public DBTraceObject getChild() { + try (LockHold hold = manager.trace.lockRead()) { + return (DBTraceObject) wrapped.getValue(); + } + } + + @Override + public void setLifespan(Lifespan lifespan) { + setLifespan(lifespan, ConflictResolution.TRUNCATE); + } + + @Override + public void setLifespan(Lifespan lifespan, ConflictResolution resolution) { + try (LockHold hold = getTrace().lockWrite()) { + if (getParent() == null) { + throw new IllegalArgumentException("Cannot set lifespan of root value"); + } + if (resolution == ConflictResolution.DENY) { + getParent().doCheckConflicts(lifespan, getEntryKey(), getValue()); + } + else if (resolution == ConflictResolution.ADJUST) { + lifespan = getParent().doAdjust(lifespan, getEntryKey(), getValue()); + } + new ValueLifespanSetter(lifespan, getValue(), this) { + @Override + protected Iterable getIntersecting(Long lower, + Long upper) { + return StreamUtils.iter(getParent().streamValuesR( + Lifespan.span(lower, upper), getEntryKey(), true).filter(v -> v != keep)); + } + + @Override + protected DBTraceObjectValue create(Lifespan range, Object value) { + return getParent().doCreateValue(range, getEntryKey(), value); + } + }.set(lifespan, getValue()); + if (isObject()) { + DBTraceObject child = getChild(); + child.emitEvents( + new TraceChangeRecord<>(TraceEvents.OBJECT_LIFE_CHANGED, null, child)); + } + } + } + + void doSetLifespan(Lifespan lifespan) { + if (wrapped.getLifespan().equals(lifespan)) { + return; + } + DBTraceObject parent = wrapped.getParent(); + DBTraceObject child = wrapped.getChildOrNull(); + parent.notifyValueDeleted(this); + if (child != null) { + child.notifyParentValueDeleted(this); + } + wrapped.doSetLifespan(lifespan); + parent.notifyValueCreated(this); + if (child != null) { + child.notifyParentValueCreated(this); + } + } + + DBTraceObjectValue doTruncateOrDeleteAndEmitLifeChange(Lifespan span) { + if (!isCanonical()) { + return doTruncateOrDelete(span); + } + DBTraceObject child = wrapped.getChildOrNull(); + DBTraceObjectValue result = doTruncateOrDelete(span); + child.emitEvents(new TraceChangeRecord<>(TraceEvents.OBJECT_LIFE_CHANGED, null, child)); + return result; + } + + DBTraceObjectValue doTruncateOrDelete(Lifespan span) { + List removed = getLifespan().subtract(span); + if (removed.isEmpty()) { + doDeleteAndEmit(); + return null; + } + doSetLifespanAndEmit(removed.get(0)); + if (removed.size() == 2) { + return getParent().doCreateValue(removed.get(1), getEntryKey(), getValue()); + } + return this; + } + + @Override + public DBTraceObject getParent() { + try (LockHold hold = manager.trace.lockRead()) { + return wrapped.getParent(); + } + } + + protected Stream doStreamVisitor(Lifespan span, + Visitor visitor) { + return TreeTraversal.INSTANCE.walkValue(visitor, this, span, null); + } + + public TraceObjectValueStorage getWrapped() { + return wrapped; + } +} diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectValueBehind.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectValueBehind.java new file mode 100644 index 0000000000..19596f24e6 --- /dev/null +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectValueBehind.java @@ -0,0 +1,101 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.trace.database.target; + +import java.util.Objects; + +import ghidra.trace.model.Lifespan; + +public class DBTraceObjectValueBehind implements TraceObjectValueStorage { + private final DBTraceObjectManager manager; + + private final DBTraceObject parent; + private final String entryKey; + private Lifespan lifespan; + private final Object value; + + private boolean deleted = false; + + private final DBTraceObjectValue wrapper; + + public DBTraceObjectValueBehind(DBTraceObjectManager manager, DBTraceObject parent, + String entryKey, Lifespan lifespan, Object value) { + this.manager = manager; + + this.parent = Objects.requireNonNull(parent, "Root cannot be delayed"); + this.entryKey = entryKey; + this.lifespan = lifespan; + this.value = value; + + this.wrapper = new DBTraceObjectValue(manager, this); + } + + @Override + public String getEntryKey() { + return entryKey; + } + + @Override + public Object getValue() { + return value; + } + + @Override + public Lifespan getLifespan() { + return lifespan; + } + + @Override + public boolean isDeleted() { + return deleted; + } + + @Override + public DBTraceObjectManager getManager() { + return manager; + } + + @Override + public DBTraceObject getChildOrNull() { + if (value instanceof DBTraceObject child) { + return child; + } + return null; + } + + @Override + public void doSetLifespan(Lifespan lifespan) { + var values = manager.valueWbCache.doRemoveNoCleanup(this); + this.lifespan = lifespan; + manager.valueWbCache.doAddDirect(values, this); + } + + @Override + public void doDelete() { + deleted = true; + manager.doDeleteCachedValue(this); + } + + @Override + public DBTraceObject getParent() { + return parent; + } + + @Override + public DBTraceObjectValue getWrapper() { + return wrapper; + } +} diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectValueData.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectValueData.java index b2423263cd..f4d5264e48 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectValueData.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectValueData.java @@ -17,17 +17,12 @@ package ghidra.trace.database.target; import java.io.IOException; import java.util.Objects; -import java.util.stream.Stream; import db.DBRecord; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressRange; -import ghidra.trace.database.target.visitors.TreeTraversal; -import ghidra.trace.database.target.visitors.TreeTraversal.Visitor; import ghidra.trace.model.Lifespan; -import ghidra.trace.model.Trace; -import ghidra.trace.model.target.*; -import ghidra.util.LockHold; +import ghidra.trace.model.target.TraceObject; import ghidra.util.database.DBCachedObjectStore; import ghidra.util.database.DBCachedObjectStoreFactory.*; import ghidra.util.database.DBObjectColumn; @@ -36,8 +31,8 @@ import ghidra.util.database.spatial.DBTreeDataRecord; @DBAnnotatedObjectInfo(version = 1) public class DBTraceObjectValueData - extends DBTreeDataRecord - implements InternalTraceObjectValue, ValueShape { + extends DBTreeDataRecord + implements TraceObjectValueStorage, ValueShape { static final String TABLE_NAME = "ObjectValue"; static final String PARENT_COLUMN_NAME = "Parent"; // R*-Tree parent @@ -89,6 +84,8 @@ public class DBTraceObjectValueData protected Address address; protected AddressRange range; + private DBTraceObjectValue wrapper; + public DBTraceObjectValueData(DBTraceObjectManager manager, DBTraceObjectValueRStarTree tree, DBCachedObjectStore store, DBRecord record) { super(store, record); @@ -96,8 +93,7 @@ public class DBTraceObjectValueData this.tree = tree; } - @Override - public void doSetPrimitive(Object primitive) { + void doSetPrimitive(Object primitive) { if (primitive instanceof TraceObject) { throw new AssertionError(); } @@ -193,11 +189,6 @@ public class DBTraceObjectValueData } } - @Override - public Trace getTrace() { - return manager.trace; - } - @Override public DBTraceObject getParent() { return objParent; @@ -208,51 +199,18 @@ public class DBTraceObjectValueData return entryKey; } - protected TraceObjectKeyPath doGetCanonicalPath() { - if (objParent == null) { - return TraceObjectKeyPath.of(); - } - return objParent.getCanonicalPath().extend(entryKey); - } - - protected boolean doIsCanonical() { - if (child == null) { - return false; - } - if (objParent == null) { // We're the root - return true; - } - return doGetCanonicalPath().equals(child.getCanonicalPath()); - } - - @Override - public TraceObjectKeyPath getCanonicalPath() { - try (LockHold hold = LockHold.lock(manager.lock.readLock())) { - return doGetCanonicalPath(); - } - } - - @Override - public boolean isCanonical() { - try (LockHold hold = LockHold.lock(manager.lock.readLock())) { - return doIsCanonical(); - } - } - @Override public Object getValue() { - try (LockHold hold = manager.trace.lockRead()) { - if (child != null) { - return child; - } - if (address != null) { - return address; - } - if (range != null) { - return range; - } - return child != null ? child : primitive; + if (child != null) { + return child; } + if (address != null) { + return address; + } + if (range != null) { + return range; + } + return child != null ? child : primitive; } @Override @@ -260,64 +218,9 @@ public class DBTraceObjectValueData return (DBTraceObject) getValue(); } - @Override - public boolean isObject() { - return child != null; - } - @Override public Lifespan getLifespan() { - try (LockHold hold = manager.trace.lockRead()) { - return lifespan; - } - } - - @Override - public void setMinSnap(long minSnap) { - try (LockHold hold = manager.trace.lockWrite()) { - setLifespan(Lifespan.span(minSnap, maxSnap)); - } - } - - @Override - public long getMinSnap() { - try (LockHold hold = manager.trace.lockRead()) { - return minSnap; - } - } - - @Override - public void setMaxSnap(long maxSnap) { - try (LockHold hold = manager.trace.lockWrite()) { - setLifespan(Lifespan.span(minSnap, maxSnap)); - } - } - - @Override - public long getMaxSnap() { - try (LockHold hold = manager.trace.lockRead()) { - return maxSnap; - } - } - - @Override - public void delete() { - try (LockHold hold = LockHold.lock(manager.lock.writeLock())) { - if (objParent == null) { - throw new IllegalArgumentException("Cannot delete root value"); - } - doDeleteAndEmit(); - } - } - - @Override - public TraceObjectValue truncateOrDelete(Lifespan span) { - try (LockHold hold = LockHold.lock(manager.lock.writeLock())) { - if (objParent == null) { - throw new IllegalArgumentException("Cannot truncate or delete root value"); - } - return doTruncateOrDeleteAndEmitLifeChange(span); - } + return lifespan; } @Override @@ -335,12 +238,12 @@ public class DBTraceObjectValueData } @Override - protected void setRecordValue(InternalTraceObjectValue value) { + protected void setRecordValue(DBTraceObjectValueData value) { // Nothing. Entry is the value } @Override - protected InternalTraceObjectValue getRecordValue() { + protected DBTraceObjectValueData getRecordValue() { return this; } @@ -396,38 +299,34 @@ public class DBTraceObjectValueData @Override public void doSetLifespan(Lifespan lifespan) { - if (minSnap == lifespan.lmin() && maxSnap == lifespan.lmax()) { - return; - } + // NB. Wrapper would not call if lifespan weren't different DBTraceObjectValueRStarTree tree = this.tree; tree.doUnparentEntry(this); - objParent.notifyValueDeleted(this); - if (child != null) { - child.notifyParentValueDeleted(this); - } minSnap = lifespan.lmin(); maxSnap = lifespan.lmax(); update(MIN_SNAP_COLUMN, MAX_SNAP_COLUMN); this.lifespan = lifespan; updateBounds(); tree.doInsertDataEntry(this); - objParent.notifyValueCreated(this); - if (child != null) { - child.notifyParentValueCreated(this); - } } @Override public void doDelete() { - objParent.notifyValueDeleted(this); - if (child != null) { - child.notifyParentValueDeleted(this); - } - manager.doDeleteEdge(this); + manager.doDeleteValue(this); } - protected Stream doStreamVisitor(Lifespan span, - Visitor visitor) { - return TreeTraversal.INSTANCE.walkValue(visitor, this, span, null); + @Override + public DBTraceObjectValue getWrapper() { + if (wrapper == null) { + wrapper = new DBTraceObjectValue(manager, this); + } + return wrapper; + } + + void setWrapper(DBTraceObjectValue wrapper) { + if (this.wrapper != null) { + throw new AssertionError(); + } + this.wrapper = wrapper; } } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectValueMapAddressSetView.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectValueMapAddressSetView.java index 6f55468f27..6074afb953 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectValueMapAddressSetView.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectValueMapAddressSetView.java @@ -35,8 +35,8 @@ public class DBTraceObjectValueMapAddressSetView extends AbstractAddressSetView private final AddressFactory factory; private final ReadWriteLock lock; - private final SpatialMap map; - private final Predicate predicate; + private final SpatialMap map; + private final Predicate predicate; /** * An address set view that unions all addresses where an entry satisfying the given predicate @@ -52,8 +52,8 @@ public class DBTraceObjectValueMapAddressSetView extends AbstractAddressSetView * @param predicate a predicate to further filter entries */ public DBTraceObjectValueMapAddressSetView(AddressFactory factory, ReadWriteLock lock, - SpatialMap map, - Predicate predicate) { + SpatialMap map, + Predicate predicate) { this.factory = factory; this.lock = lock; this.map = map; @@ -63,7 +63,7 @@ public class DBTraceObjectValueMapAddressSetView extends AbstractAddressSetView @Override public boolean contains(Address addr) { try (LockHold hold = LockHold.lock(lock.readLock())) { - for (InternalTraceObjectValue value : map + for (DBTraceObjectValueData value : map .reduce(TraceObjectValueQuery.intersecting(Lifespan.ALL, new AddressRangeImpl(addr, addr))) .values()) { @@ -95,7 +95,7 @@ public class DBTraceObjectValueMapAddressSetView extends AbstractAddressSetView @Override public boolean isEmpty() { try (LockHold hold = LockHold.lock(lock.readLock())) { - for (InternalTraceObjectValue value : map.values()) { + for (DBTraceObjectValueData value : map.values()) { if (predicate.test(value)) { return false; } @@ -107,7 +107,7 @@ public class DBTraceObjectValueMapAddressSetView extends AbstractAddressSetView @Override public Address getMinAddress() { try (LockHold hold = LockHold.lock(lock.readLock())) { - for (Entry entry : map + for (Entry entry : map .reduce(TraceObjectValueQuery.all().starting(AddressDimension.FORWARD)) .orderedEntries()) { if (predicate.test(entry.getValue())) { @@ -121,7 +121,7 @@ public class DBTraceObjectValueMapAddressSetView extends AbstractAddressSetView @Override public Address getMaxAddress() { try (LockHold hold = LockHold.lock(lock.readLock())) { - for (Entry entry : map + for (Entry entry : map .reduce(TraceObjectValueQuery.all().starting(AddressDimension.BACKWARD)) .orderedEntries()) { if (predicate.test(entry.getValue())) { @@ -153,14 +153,14 @@ public class DBTraceObjectValueMapAddressSetView extends AbstractAddressSetView protected AddressRangeIterator doGetAddressRanges(RecAddress start, RecAddress end, boolean forward) { - Iterator> mapIt = map + Iterator> mapIt = map .reduce(TraceObjectValueQuery .intersecting(EntryKeyDimension.INSTANCE.absoluteMin(), EntryKeyDimension.INSTANCE.absoluteMax(), Lifespan.ALL, start, end) .starting(forward ? AddressDimension.FORWARD : AddressDimension.BACKWARD)) .orderedEntries() .iterator(); - Iterator> fltIt = + Iterator> fltIt = IteratorUtils.filteredIterator(mapIt, e -> predicate.test(e.getValue())); Iterator rawIt = IteratorUtils.transformedIterator(fltIt, e -> e.getKey().getRange(factory)); diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectValueRStarTree.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectValueRStarTree.java index 301a566ff7..5281e6b715 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectValueRStarTree.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectValueRStarTree.java @@ -36,16 +36,16 @@ public class DBTraceObjectValueRStarTree extends AbstractHyperRStarTree< // ValueTriple, // ValueShape, DBTraceObjectValueData, // ValueBox, DBTraceObjectValueNode, // - InternalTraceObjectValue, TraceObjectValueQuery> { + DBTraceObjectValueData, TraceObjectValueQuery> { public static class DBTraceObjectValueMap extends AsSpatialMap { + DBTraceObjectValueData, ValueBox, DBTraceObjectValueData, TraceObjectValueQuery> { private final AddressFactory factory; private final ReadWriteLock lock; public DBTraceObjectValueMap(AbstractConstraintsTree tree, + ValueBox, ?, DBTraceObjectValueData, TraceObjectValueQuery> tree, TraceObjectValueQuery query, AddressFactory factory, ReadWriteLock lock) { super(tree, query); this.factory = factory; @@ -59,7 +59,7 @@ public class DBTraceObjectValueRStarTree extends AbstractHyperRStarTree< // } public AddressSetView getAddressSetView(Lifespan at, - Predicate predicate) { + Predicate predicate) { return new DBTraceObjectValueMapAddressSetView(factory, lock, this.reduce(TraceObjectValueQuery.intersecting( EntryKeyDimension.INSTANCE.absoluteMin(), diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectValueWriteBehindCache.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectValueWriteBehindCache.java new file mode 100644 index 0000000000..57387f52d3 --- /dev/null +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectValueWriteBehindCache.java @@ -0,0 +1,380 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.trace.database.target; + +import java.util.*; +import java.util.concurrent.ExecutionException; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import db.Transaction; +import ghidra.async.AsyncReference; +import ghidra.program.model.address.*; +import ghidra.trace.model.Lifespan; +import ghidra.trace.model.target.TraceObjectInterface; +import ghidra.trace.model.target.TraceObjectKeyPath; +import ghidra.util.*; + +class DBTraceObjectValueWriteBehindCache { + public static final int INITIAL_CACHE_SIZE = 1000; + public static final int BATCH_SIZE = 100; + public static final int DELAY_MS = 10000; + + private final DBTraceObjectManager manager; + private final Thread worker; + private volatile long mark = 0; + private final AsyncReference busy = new AsyncReference<>(false); + private volatile boolean flushing = false; + + private final Map>> cachedValues = + new HashMap<>(); + + public DBTraceObjectValueWriteBehindCache(DBTraceObjectManager manager) { + this.manager = manager; + + worker = new Thread(this::workLoop, "WriteBehind for " + manager.trace.getName()); + worker.start(); + } + + private void workLoop() { + while (!manager.trace.isClosed()) { + try { + synchronized (cachedValues) { + if (cachedValues.isEmpty()) { + busy.set(false, null); + flushing = false; + cachedValues.wait(); + } + while (!flushing) { + long left = mark - System.currentTimeMillis(); + if (left <= 0) { + break; + } + Msg.trace(this, + "Waiting %d ms. Cache is %d big".formatted(left, cachedValues.size())); + cachedValues.wait(left); + } + } + if (manager.trace.isClosed()) { + break; + } + writeBatch(); + if (!flushing && !manager.trace.isClosing()) { + Thread.sleep(100); + } + } + catch (InterruptedException e) { + } + } + busy.set(false, null); + flushing = false; + } + + private List getBatch() { + synchronized (cachedValues) { + return doStreamAllValues() + .limit(BATCH_SIZE) + .toList(); + } + } + + private Stream doStreamAllValues() { + return cachedValues.values() + .stream() + .flatMap(v -> v.values().stream()) + .flatMap(v -> v.values().stream()); + } + + private void doAdd(DBTraceObjectValueBehind behind) { + var keys = cachedValues.computeIfAbsent(behind.getParent(), k -> new HashMap<>()); + var values = keys.computeIfAbsent(behind.getEntryKey(), k -> new TreeMap<>()); + values.put(behind.getLifespan().min(), behind); + } + + NavigableMap doRemoveNoCleanup( + DBTraceObjectValueBehind behind) { + var keys = cachedValues.get(behind.getParent()); + var values = keys.get(behind.getEntryKey()); + values.remove(behind.getLifespan().min()); + return values; + } + + void doAddDirect(NavigableMap values, + DBTraceObjectValueBehind b) { + values.put(b.getLifespan().min(), b); + } + + private void doRemove(DBTraceObjectValueBehind behind) { + var keys = cachedValues.get(behind.getParent()); + var values = keys.get(behind.getEntryKey()); + values.remove(behind.getLifespan().min()); + if (values.isEmpty()) { + keys.remove(behind.getEntryKey()); + if (keys.isEmpty()) { + cachedValues.remove(behind.getParent()); + } + } + } + + private void writeBatch() { + try (Transaction tx = manager.trace.openTransaction("Write Behind")) { + try (LockHold hold = LockHold.lock(manager.lock.writeLock())) { + for (DBTraceObjectValueBehind behind : getBatch()) { + synchronized (cachedValues) { + doRemove(behind); + } + DBTraceObjectValueData value = manager.doCreateValueData(behind.getLifespan(), + behind.getParent(), behind.getEntryKey(), behind.getValue()); + behind.getWrapper().setWrapped(value); + } + } + } + manager.trace.clearUndo(); + Msg.trace(this, "Wrote a batch. %d parents remain.".formatted(cachedValues.size())); + } + + public DBTraceObjectValueBehind doCreateValue(Lifespan lifespan, DBTraceObject parent, + String key, Object value) { + if (manager.trace.isClosing()) { + throw new IllegalStateException("Trace is closing"); + } + DBTraceObjectValueBehind entry = + new DBTraceObjectValueBehind(manager, parent, key, lifespan, + manager.validateValue(value)); + synchronized (cachedValues) { + doAdd(entry); + mark = System.currentTimeMillis() + DELAY_MS; + busy.set(true, null); + cachedValues.notify(); + } + return entry; + } + + public void remove(DBTraceObjectValueBehind value) { + synchronized (cachedValues) { + doRemove(value); + } + } + + public void clear() { + synchronized (cachedValues) { + cachedValues.clear(); + } + } + + public Stream streamAllValues() { + return doStreamAllValues(); + } + + public DBTraceObjectValueBehind get(DBTraceObject parent, String key, long snap) { + var keys = cachedValues.get(parent); + if (keys == null) { + return null; + } + var values = keys.get(key); + if (values == null) { + return null; + } + + var floor = values.floorEntry(snap); + if (floor == null) { + return null; + } + + if (!floor.getValue().getLifespan().contains(snap)) { + return null; + } + return floor.getValue(); + } + + public Stream streamParents(DBTraceObject child, Lifespan lifespan) { + // TODO: Optimize/index this? + return streamAllValues() + .filter(v -> v.getValue() == child && v.getLifespan().intersects(lifespan)); + } + + private Stream streamSub( + NavigableMap map, Lifespan span, boolean forward) { + Long floor = map.floorKey(span.min()); + if (floor == null) { + floor = span.min(); + } + var sub = map.subMap(floor, true, span.max(), true); + if (!forward) { + sub = sub.descendingMap(); + } + return sub.values().stream(); + } + + public Stream streamCanonicalParents(DBTraceObject child, + Lifespan lifespan) { + TraceObjectKeyPath path = child.getCanonicalPath(); + if (path.isRoot()) { + return Stream.of(); + } + String entryKey = path.key(); + // TODO: Better indexing? + return cachedValues.values() + .stream() + .flatMap(v -> v.entrySet() + .stream() + .filter(e -> entryKey.equals(e.getKey())) + .map(e -> e.getValue())) + .flatMap(v -> streamSub(v, lifespan, true)); + } + + public Stream streamValues(DBTraceObject parent, Lifespan lifespan) { + // TODO: Better indexing? + var keys = cachedValues.get(parent); + if (keys == null) { + return Stream.of(); + } + return keys.values().stream().flatMap(v -> streamSub(v, lifespan, true)); + } + + public Stream streamValues(DBTraceObject parent, String key, + Lifespan lifespan, boolean forward) { + var keys = cachedValues.get(parent); + if (keys == null) { + return Stream.of(); + } + var values = keys.get(key); + if (values == null) { + return Stream.of(); + } + return streamSub(values, lifespan, forward); + } + + static boolean intersectsRange(Object value, AddressRange range) { + return (value instanceof Address av && range.contains(av)) || + (value instanceof AddressRange rv && range.intersects(rv)); + } + + private Stream streamValuesIntersectingLifespan(Lifespan lifespan, + String entryKey) { + // TODO: In-memory spatial index? + var top = cachedValues.values().stream(); + var keys = entryKey == null + ? top.flatMap(v -> v.values().stream()) + : top.flatMap(v -> v.entrySet() + .stream() + .filter(e -> entryKey.equals(e.getKey())) + .map(e -> e.getValue())); + return keys.flatMap(v -> streamSub(v, lifespan, true)); + } + + public Stream streamValuesIntersecting(Lifespan lifespan, + AddressRange range, String entryKey) { + return streamValuesIntersectingLifespan(lifespan, entryKey) + .filter(v -> intersectsRange(v.getValue(), range)); + } + + static boolean atAddress(Object value, Address address) { + return (value instanceof Address av && address.equals(av)) || + (value instanceof AddressRange rv && rv.contains(address)); + } + + public Stream streamValuesAt(long snap, Address address, + String entryKey) { + return streamValuesIntersectingLifespan(Lifespan.at(snap), entryKey) + .filter(v -> atAddress(v.getValue(), address)); + } + + static AddressRange getIfRangeOrAddress(Object v) { + if (v instanceof AddressRange rv) { + return rv; + } + if (v instanceof Address av) { + return new AddressRangeImpl(av, av); + } + return null; + } + + public AddressSetView getObjectsAddresSet(long snap, + String key, Class ifaceCls, Predicate predicate) { + return new AbstractAddressSetView() { + AddressSet collectRanges() { + AddressSet result = new AddressSet(); + for (DBTraceObjectValueBehind v : StreamUtils + .iter(streamValuesIntersectingLifespan(Lifespan.at(snap), key))) { + AddressRange range = getIfRangeOrAddress(v.getValue()); + if (range == null) { + continue; + } + if (!DBTraceObjectManager.acceptValue(v.getWrapper(), key, ifaceCls, + predicate)) { + continue; + } + result.add(range); + } + return result; + } + + @Override + public boolean contains(Address addr) { + for (DBTraceObjectValueBehind v : StreamUtils + .iter(streamValuesIntersectingLifespan(Lifespan.at(snap), key))) { + if (!addr.equals(v.getValue())) { + continue; + } + if (!DBTraceObjectManager.acceptValue(v.getWrapper(), key, ifaceCls, + predicate)) { + continue; + } + return true; + } + return false; + } + + @Override + public AddressRangeIterator getAddressRanges() { + return collectRanges().getAddressRanges(); + } + + @Override + public AddressRangeIterator getAddressRanges(boolean forward) { + return collectRanges().getAddressRanges(forward); + } + + @Override + public AddressRangeIterator getAddressRanges(Address start, boolean forward) { + // TODO: Could cull during collection + return collectRanges().getAddressRanges(start, forward); + } + }; + } + + public void flush() { + flushing = true; + worker.interrupt(); + try { + busy.waitValue(false).get(); + } + catch (InterruptedException | ExecutionException e) { + throw new AssertionError(e); + } + } + + public void waitWorkers() { + worker.interrupt(); + try { + worker.join(10000); + } + catch (InterruptedException e) { + throw new AssertionError(e); + } + } +} diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/InternalTraceObjectValue.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/InternalTraceObjectValue.java deleted file mode 100644 index c1da4d146f..0000000000 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/InternalTraceObjectValue.java +++ /dev/null @@ -1,198 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package ghidra.trace.database.target; - -import java.util.*; - -import org.apache.commons.collections4.IterableUtils; - -import ghidra.trace.database.DBTraceUtils.LifespanMapSetter; -import ghidra.trace.model.Lifespan; -import ghidra.trace.model.target.TraceObject.ConflictResolution; -import ghidra.trace.model.target.TraceObjectValue; -import ghidra.trace.util.TraceChangeRecord; -import ghidra.trace.util.TraceEvents; -import ghidra.util.LockHold; - -interface InternalTraceObjectValue extends TraceObjectValue { - abstract class ValueLifespanSetter - extends LifespanMapSetter { - protected final Lifespan range; - protected final Object value; - protected InternalTraceObjectValue keep = null; - protected Collection kept = new ArrayList<>(2); - - public ValueLifespanSetter(Lifespan range, Object value) { - this.range = range; - this.value = value; - } - - public ValueLifespanSetter(Lifespan range, Object value, - InternalTraceObjectValue keep) { - this(range, value); - this.keep = keep; - } - - @Override - protected Lifespan getRange(InternalTraceObjectValue entry) { - return entry.getLifespan(); - } - - @Override - protected Object getValue(InternalTraceObjectValue entry) { - return entry.getValue(); - } - - @Override - protected void remove(InternalTraceObjectValue entry) { - if (Objects.equals(entry.getValue(), value)) { - if (keep == null) { - keep = entry; - } - else { - entry.doDeleteAndEmit(); - } - } - else { - InternalTraceObjectValue created = entry.doTruncateOrDelete(range); - if (!entry.isDeleted()) { - kept.add(entry); - } - if (created != null) { - kept.add(created); - } - } - } - - @Override - protected InternalTraceObjectValue put(Lifespan range, Object value) { - if (value == null) { - return null; - } - if (keep != null && Objects.equals(this.value, value)) { - keep.doSetLifespanAndEmit(range); - return keep; - } - for (InternalTraceObjectValue k : kept) { - if (Objects.equals(value, k.getValue()) && Objects.equals(range, k.getLifespan())) { - kept.remove(k); - return k; - } - } - return create(range, value); - } - - protected abstract InternalTraceObjectValue create(Lifespan range, Object value); - } - - void doSetPrimitive(Object primitive); - - DBTraceObjectManager getManager(); - - /** - * Get the database key - * - * @return the key - */ - long getKey(); - - @Override - DBTraceObject getChild(); - - DBTraceObject getChildOrNull(); - - void doSetLifespan(Lifespan lifespan); - - default void doSetLifespanAndEmit(Lifespan lifespan) { - Lifespan oldLifespan = getLifespan(); - doSetLifespan(lifespan); - getParent().emitEvents(new TraceChangeRecord<>(TraceEvents.VALUE_LIFESPAN_CHANGED, null, - this, oldLifespan, lifespan)); - } - - @Override - default void setLifespan(Lifespan lifespan) { - setLifespan(lifespan, ConflictResolution.TRUNCATE); - } - - @Override - default void setLifespan(Lifespan lifespan, ConflictResolution resolution) { - try (LockHold hold = getTrace().lockWrite()) { - if (getParent() == null) { - throw new IllegalArgumentException("Cannot set lifespan of root value"); - } - if (resolution == ConflictResolution.DENY) { - getParent().doCheckConflicts(lifespan, getEntryKey(), getValue()); - } - else if (resolution == ConflictResolution.ADJUST) { - lifespan = getParent().doAdjust(lifespan, getEntryKey(), getValue()); - } - new ValueLifespanSetter(lifespan, getValue(), this) { - @Override - protected Iterable getIntersecting(Long lower, - Long upper) { - Collection col = Collections.unmodifiableCollection( - getParent().doGetValues(Lifespan.span(lower, upper), getEntryKey(), true)); - return IterableUtils.filteredIterable(col, v -> v != keep); - } - - @Override - protected InternalTraceObjectValue create(Lifespan range, Object value) { - return getParent().doCreateValue(range, getEntryKey(), value); - } - }.set(lifespan, getValue()); - if (isObject()) { - DBTraceObject child = getChild(); - child.emitEvents( - new TraceChangeRecord<>(TraceEvents.OBJECT_LIFE_CHANGED, null, child)); - } - } - } - - void doDelete(); - - default void doDeleteAndEmit() { - DBTraceObject parent = getParent(); - doDelete(); - parent.emitEvents(new TraceChangeRecord<>(TraceEvents.VALUE_DELETED, null, this)); - } - - @Override - DBTraceObject getParent(); - - default InternalTraceObjectValue doTruncateOrDeleteAndEmitLifeChange(Lifespan span) { - if (!isCanonical()) { - return doTruncateOrDelete(span); - } - DBTraceObject child = getChildOrNull(); - InternalTraceObjectValue result = doTruncateOrDelete(span); - child.emitEvents(new TraceChangeRecord<>(TraceEvents.OBJECT_LIFE_CHANGED, null, child)); - return result; - } - - default InternalTraceObjectValue doTruncateOrDelete(Lifespan span) { - List removed = getLifespan().subtract(span); - if (removed.isEmpty()) { - doDeleteAndEmit(); - return null; - } - doSetLifespanAndEmit(removed.get(0)); - if (removed.size() == 2) { - return getParent().doCreateValue(removed.get(1), getEntryKey(), getValue()); - } - return this; - } -} diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/TraceObjectValueStorage.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/TraceObjectValueStorage.java new file mode 100644 index 0000000000..b57d001f2c --- /dev/null +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/TraceObjectValueStorage.java @@ -0,0 +1,48 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.trace.database.target; + +import ghidra.trace.model.Lifespan; + +interface TraceObjectValueStorage { + DBTraceObjectManager getManager(); + + DBTraceObjectValue getWrapper(); + + DBTraceObject getParent(); + + String getEntryKey(); + + /** + * Just set the lifespan, no notifications + * + *

+ * The wrapper will notify the parent and child, if necessary. + * + * @param lifespan the new lifespan + */ + void doSetLifespan(Lifespan lifespan); + + Lifespan getLifespan(); + + DBTraceObject getChildOrNull(); + + Object getValue(); + + boolean isDeleted(); + + void doDelete(); +} diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/thread/DBTraceThreadManager.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/thread/DBTraceThreadManager.java index 6073901aaf..56277ed82f 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/thread/DBTraceThreadManager.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/thread/DBTraceThreadManager.java @@ -77,10 +77,9 @@ public class DBTraceThreadManager implements TraceThreadManager, DBTraceManager if (objectManager.hasSchema()) { return objectManager.assertMyThread(thread); } - if (!(thread instanceof DBTraceThread)) { + if (!(thread instanceof DBTraceThread dbThread)) { throw new IllegalArgumentException("Thread " + thread + " is not part of this trace"); } - DBTraceThread dbThread = (DBTraceThread) thread; if (dbThread.manager != this) { throw new IllegalArgumentException("Thread " + thread + " is not part of this trace"); } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/target/TraceObjectManager.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/target/TraceObjectManager.java index 6da8c977eb..c7bac4fd15 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/target/TraceObjectManager.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/target/TraceObjectManager.java @@ -123,16 +123,23 @@ public interface TraceObjectManager { /** * Get all the objects in the database * - * @return the collection of all objects + * @return the stream of all objects */ - Collection getAllObjects(); + Stream getAllObjects(); + + /** + * Get the number of objects in the database + * + * @return the number of objects + */ + int getObjectCount(); /** * Get all the values (edges) in the database * - * @return the collect of all values + * @return the stream of all values */ - Collection getAllValues(); + Stream getAllValues(); /** * Get all address-ranged values intersecting the given span and address range diff --git a/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/ToyDBTraceBuilder.java b/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/ToyDBTraceBuilder.java index b3524eae0d..72a429dc5a 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/ToyDBTraceBuilder.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/ToyDBTraceBuilder.java @@ -729,6 +729,7 @@ public class ToyDBTraceBuilder implements AutoCloseable { public File save() throws IOException, CancelledException { Path tmp = Files.createTempFile("test", ".db"); Files.delete(tmp); // saveAs must create the file + trace.objectManager.flushWbCaches(); trace.getDBHandle().saveAs(tmp.toFile(), false, new ConsoleTaskMonitor()); return tmp.toFile(); } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/target/DBTraceObjectManagerTest.java b/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/target/DBTraceObjectManagerTest.java index 26c55c8a96..1af4b18767 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/target/DBTraceObjectManagerTest.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/target/DBTraceObjectManagerTest.java @@ -327,42 +327,42 @@ public class DBTraceObjectManagerTest extends AbstractGhidraHeadlessIntegrationT @Test public void testClear() { populateModel(3); - assertEquals(5, manager.getAllObjects().size()); + assertEquals(5, manager.getObjectCount()); try (Transaction tx = b.startTransaction()) { manager.clear(); } - assertEquals(0, manager.getAllObjects().size()); + assertEquals(0, manager.getObjectCount()); populateModel(3); - assertEquals(5, manager.getAllObjects().size()); + assertEquals(5, manager.getObjectCount()); } - @Test + // @Test // Write-behind cache does not implement undo or redo public void testUndoRedo() throws Exception { populateModel(3); - assertEquals(5, manager.getAllObjects().size()); + assertEquals(5, manager.getObjectCount()); b.trace.undo(); - assertEquals(0, manager.getAllObjects().size()); + assertEquals(0, manager.getObjectCount()); b.trace.redo(); - assertEquals(5, manager.getAllObjects().size()); + assertEquals(5, manager.getObjectCount()); } - @Test + // @Test // Write-behind cache does not implement abort public void testAbort() throws Exception { try (Transaction tx = b.startTransaction()) { populateModel(3); - assertEquals(5, manager.getAllObjects().size()); + assertEquals(5, manager.getObjectCount()); tx.abort(); } - assertEquals(0, manager.getAllObjects().size()); + assertEquals(0, manager.getObjectCount()); populateModel(3); - assertEquals(5, manager.getAllObjects().size()); + assertEquals(5, manager.getObjectCount()); } @Test diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/StreamUtils.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/StreamUtils.java index 6f89d6cddc..292f40a9ac 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/StreamUtils.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/StreamUtils.java @@ -15,13 +15,14 @@ */ package ghidra.util; -import java.util.Collection; -import java.util.Comparator; +import java.util.*; import java.util.stream.Stream; import java.util.stream.StreamSupport; -public enum StreamUtils { - ; +public class StreamUtils { + private StreamUtils() { + } + @SuppressWarnings("unchecked") public static Stream merge(Collection> streams, Comparator comparator) { @@ -31,4 +32,9 @@ public enum StreamUtils { return StreamSupport.stream(new MergeSortingSpliterator<>( streams.stream().map(s -> s.spliterator()).toList(), comparator), false); } + + @SuppressWarnings("unchecked") + public static Iterable iter(Stream stream) { + return () -> (Iterator) stream.iterator(); + } } diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/database/DBSynchronizedCollection.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/database/DBSynchronizedCollection.java new file mode 100644 index 0000000000..931d72c002 --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/database/DBSynchronizedCollection.java @@ -0,0 +1,123 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.util.database; + +import java.util.Collection; +import java.util.Iterator; +import java.util.concurrent.locks.ReadWriteLock; + +import ghidra.util.LockHold; + +public class DBSynchronizedCollection implements Collection { + private final Collection delegate; + private final ReadWriteLock lock; + + public DBSynchronizedCollection(Collection delegate, ReadWriteLock lock) { + this.delegate = delegate; + this.lock = lock; + } + + @Override + public int size() { + try (LockHold hold = LockHold.lock(lock.readLock())) { + return delegate.size(); + } + } + + @Override + public boolean isEmpty() { + try (LockHold hold = LockHold.lock(lock.readLock())) { + return delegate.isEmpty(); + } + } + + @Override + public boolean contains(Object o) { + try (LockHold hold = LockHold.lock(lock.readLock())) { + return delegate.contains(o); + } + } + + @Override + public Iterator iterator() { + try (LockHold hold = LockHold.lock(lock.readLock())) { + return new DBSynchronizedIterator<>(delegate.iterator(), lock); + } + } + + @Override + public Object[] toArray() { + try (LockHold hold = LockHold.lock(lock.readLock())) { + return delegate.toArray(); + } + } + + @Override + public T[] toArray(T[] a) { + try (LockHold hold = LockHold.lock(lock.readLock())) { + return delegate.toArray(a); + } + } + + @Override + public boolean add(E e) { + try (LockHold hold = LockHold.lock(lock.writeLock())) { + return delegate.add(e); + } + } + + @Override + public boolean remove(Object o) { + try (LockHold hold = LockHold.lock(lock.writeLock())) { + return delegate.remove(o); + } + } + + @Override + public boolean containsAll(Collection c) { + try (LockHold hold = LockHold.lock(lock.readLock())) { + return delegate.containsAll(c); + } + } + + @Override + public boolean addAll(Collection c) { + try (LockHold hold = LockHold.lock(lock.writeLock())) { + return delegate.addAll(c); + } + } + + @Override + public boolean removeAll(Collection c) { + try (LockHold hold = LockHold.lock(lock.writeLock())) { + return delegate.removeAll(c); + } + } + + @Override + public boolean retainAll(Collection c) { + try (LockHold hold = LockHold.lock(lock.writeLock())) { + return delegate.retainAll(c); + } + } + + @Override + public void clear() { + try (LockHold hold = LockHold.lock(lock.writeLock())) { + delegate.clear(); + } + } +} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/database/DBSynchronizedIterator.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/database/DBSynchronizedIterator.java new file mode 100644 index 0000000000..95387a9016 --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/database/DBSynchronizedIterator.java @@ -0,0 +1,52 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.util.database; + +import java.util.Iterator; +import java.util.concurrent.locks.ReadWriteLock; + +import ghidra.util.LockHold; + +public class DBSynchronizedIterator implements Iterator { + private final Iterator iterator; + private final ReadWriteLock lock; + + public DBSynchronizedIterator(Iterator iterator, ReadWriteLock lock) { + this.iterator = iterator; + this.lock = lock; + } + + @Override + public boolean hasNext() { + try (LockHold hold = LockHold.lock(lock.readLock())) { + return iterator.hasNext(); + } + } + + @Override + public T next() { + try (LockHold hold = LockHold.lock(lock.readLock())) { + return iterator.next(); + } + } + + @Override + public void remove() { + try (LockHold hold = LockHold.lock(lock.writeLock())) { + iterator.remove(); + } + } +} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/database/spatial/AbstractConstraintsTreeSpatialMap.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/database/spatial/AbstractConstraintsTreeSpatialMap.java index 206a7000ae..c1b613f89e 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/database/spatial/AbstractConstraintsTreeSpatialMap.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/database/spatial/AbstractConstraintsTreeSpatialMap.java @@ -18,12 +18,12 @@ package ghidra.util.database.spatial; import java.lang.reflect.Array; import java.util.*; import java.util.Map.Entry; -import java.util.concurrent.locks.ReadWriteLock; import java.util.function.Consumer; import org.apache.commons.collections4.IteratorUtils; import ghidra.util.LockHold; +import ghidra.util.database.DBSynchronizedIterator; import ghidra.util.database.spatial.DBTreeDataRecord.RecordEntry; public abstract class AbstractConstraintsTreeSpatialMap< // @@ -98,37 +98,6 @@ public abstract class AbstractConstraintsTreeSpatialMap< // return (int) l; } - protected static class SynchronizedIterator implements Iterator { - private final Iterator iterator; - private final ReadWriteLock lock; - - public SynchronizedIterator(Iterator iterator, ReadWriteLock lock) { - this.iterator = iterator; - this.lock = lock; - } - - @Override - public boolean hasNext() { - try (LockHold hold = LockHold.lock(lock.readLock())) { - return iterator.hasNext(); - } - } - - @Override - public T next() { - try (LockHold hold = LockHold.lock(lock.readLock())) { - return iterator.next(); - } - } - - @Override - public void remove() { - try (LockHold hold = LockHold.lock(lock.writeLock())) { - iterator.remove(); - } - } - } - protected abstract static class ToArrayConsumer implements Consumer { protected final A[] arr; protected int i = 0; @@ -172,7 +141,7 @@ public abstract class AbstractConstraintsTreeSpatialMap< // @Override public Iterator> iterator() { try (LockHold hold = LockHold.lock(tree.dataStore.readLock())) { - return new SynchronizedIterator<>( + return new DBSynchronizedIterator<>( IteratorUtils.transformedIterator(tree.iterator(query), r -> r.asEntry()), tree.dataStore.getLock()); } @@ -220,6 +189,12 @@ public abstract class AbstractConstraintsTreeSpatialMap< // public boolean isEmpty() { return AbstractConstraintsTreeSpatialMap.this.isEmpty(); } + + @Override + public Spliterator> spliterator() { + // Size estimate is more expensive than benefits of knowing size + return Spliterators.spliteratorUnknownSize(iterator(), 0); + } }; } @@ -229,7 +204,7 @@ public abstract class AbstractConstraintsTreeSpatialMap< // @Override public Iterator> iterator() { try (LockHold hold = LockHold.lock(tree.dataStore.readLock())) { - return new SynchronizedIterator<>( + return new DBSynchronizedIterator<>( IteratorUtils.transformedIterator(tree.orderedIterator(query), r -> r.asEntry()), tree.dataStore.getLock()); @@ -245,6 +220,12 @@ public abstract class AbstractConstraintsTreeSpatialMap< // public boolean isEmpty() { return AbstractConstraintsTreeSpatialMap.this.isEmpty(); } + + @Override + public Spliterator> spliterator() { + // Size estimate is more expensive than benefits of knowing size + return Spliterators.spliteratorUnknownSize(iterator(), 0); + } }; } @@ -254,7 +235,7 @@ public abstract class AbstractConstraintsTreeSpatialMap< // @Override public Iterator iterator() { try (LockHold hold = LockHold.lock(tree.dataStore.readLock())) { - return new SynchronizedIterator<>( + return new DBSynchronizedIterator<>( IteratorUtils.transformedIterator(tree.iterator(query), r -> r.getShape()), tree.dataStore.getLock()); } @@ -301,6 +282,12 @@ public abstract class AbstractConstraintsTreeSpatialMap< // public boolean isEmpty() { return AbstractConstraintsTreeSpatialMap.this.isEmpty(); } + + @Override + public Spliterator spliterator() { + // Size estimate is more expensive than benefits of knowing size + return Spliterators.spliteratorUnknownSize(iterator(), 0); + } }; } @@ -310,7 +297,7 @@ public abstract class AbstractConstraintsTreeSpatialMap< // @Override public Iterator iterator() { try (LockHold hold = LockHold.lock(tree.dataStore.readLock())) { - return new SynchronizedIterator<>( + return new DBSynchronizedIterator<>( IteratorUtils.transformedIterator(tree.orderedIterator(query), r -> r.getShape()), tree.dataStore.getLock()); @@ -326,6 +313,12 @@ public abstract class AbstractConstraintsTreeSpatialMap< // public boolean isEmpty() { return AbstractConstraintsTreeSpatialMap.this.isEmpty(); } + + @Override + public Spliterator spliterator() { + // Size estimate is more expensive than benefits of knowing size + return Spliterators.spliteratorUnknownSize(iterator(), 0); + } }; } @@ -335,7 +328,7 @@ public abstract class AbstractConstraintsTreeSpatialMap< // @Override public Iterator iterator() { try (LockHold hold = LockHold.lock(tree.dataStore.readLock())) { - return new SynchronizedIterator<>( + return new DBSynchronizedIterator<>( IteratorUtils.transformedIterator(tree.iterator(query), r -> r.getRecordValue()), tree.dataStore.getLock()); @@ -383,6 +376,12 @@ public abstract class AbstractConstraintsTreeSpatialMap< // public boolean isEmpty() { return AbstractConstraintsTreeSpatialMap.this.isEmpty(); } + + @Override + public Spliterator spliterator() { + // Size estimate is more expensive than benefits of knowing size + return Spliterators.spliteratorUnknownSize(iterator(), 0); + } }; } @@ -392,7 +391,7 @@ public abstract class AbstractConstraintsTreeSpatialMap< // @Override public Iterator iterator() { try (LockHold hold = LockHold.lock(tree.dataStore.readLock())) { - return new SynchronizedIterator<>( + return new DBSynchronizedIterator<>( IteratorUtils.transformedIterator(tree.orderedIterator(query), r -> r.getRecordValue()), tree.dataStore.getLock()); @@ -408,6 +407,12 @@ public abstract class AbstractConstraintsTreeSpatialMap< // public boolean isEmpty() { return AbstractConstraintsTreeSpatialMap.this.isEmpty(); } + + @Override + public Spliterator spliterator() { + // Size estimate is more expensive than benefits of knowing size + return Spliterators.spliteratorUnknownSize(iterator(), 0); + } }; } diff --git a/Ghidra/Debug/ProposedUtils/src/test/java/ghidra/util/database/spatial/RStarTreeMapTest.java b/Ghidra/Debug/ProposedUtils/src/test/java/ghidra/util/database/spatial/RStarTreeMapTest.java index 960b770ba4..eb48a817d4 100644 --- a/Ghidra/Debug/ProposedUtils/src/test/java/ghidra/util/database/spatial/RStarTreeMapTest.java +++ b/Ghidra/Debug/ProposedUtils/src/test/java/ghidra/util/database/spatial/RStarTreeMapTest.java @@ -629,9 +629,9 @@ public class RStarTreeMapTest { private static final int MAX_CHILDREN = 5; private final DBCachedObjectStoreFactory storeFactory; private final IntRStarTree tree; - private final SpatialMap map; + public final SpatialMap map; - protected MyDomainObject(Object consumer) throws IOException, VersionException { + public MyDomainObject(Object consumer) throws IOException, VersionException { super(new DBHandle(), DBOpenMode.CREATE, new ConsoleTaskMonitor(), "Testing", 500, 1000, consumer); storeFactory = new DBCachedObjectStoreFactory(this); diff --git a/Ghidra/Framework/Emulation/src/main/java/generic/RangeMapSetter.java b/Ghidra/Framework/Emulation/src/main/java/generic/RangeMapSetter.java index 5017e282b6..441855cba4 100644 --- a/Ghidra/Framework/Emulation/src/main/java/generic/RangeMapSetter.java +++ b/Ghidra/Framework/Emulation/src/main/java/generic/RangeMapSetter.java @@ -52,6 +52,17 @@ public abstract class RangeMapSetter { */ protected abstract V getValue(E entry); + /** + * Check if two values are equal + * + * @param v1 the first value + * @param v2 the second value + * @return true if equal, false if not + */ + protected boolean valuesEqual(V v1, V v2) { + return Objects.equals(v1, v2); + } + /** * Remove an entry from the map * @@ -190,7 +201,7 @@ public abstract class RangeMapSetter { R r = getRange(entry); int cmpMin = compare(getLower(r), lower); int cmpMax = compare(getUpper(r), upper); - boolean sameVal = Objects.equals(getValue(entry), value); + boolean sameVal = valuesEqual(getValue(entry), value); if (cmpMin <= 0 && cmpMax >= 0 && sameVal) { return entry; // The value in this range is already set as specified } diff --git a/Ghidra/Framework/Emulation/src/main/java/generic/Span.java b/Ghidra/Framework/Emulation/src/main/java/generic/Span.java index 6d896683e6..66f3928e4a 100644 --- a/Ghidra/Framework/Emulation/src/main/java/generic/Span.java +++ b/Ghidra/Framework/Emulation/src/main/java/generic/Span.java @@ -32,7 +32,7 @@ import generic.ULongSpan.*; * impose behaviors and properties that aren't otherwise present on the type of endpoints. For * example, the domain may be {@link Long}s, but using unsigned attributes. The domain also provides * a factory for new spans. While nominally, this only supports closed intervals, the domain can - * define a custom endpoint type to obtain mixed intervals, as in {@link End}. + * define a custom endpoint type to obtain mixed intervals. * * @param the type of endpoints * @param the type of spans (recursive) @@ -387,7 +387,7 @@ public interface Span> extends Comparable { * existing entries are truncated or deleted (or coalesced if they share the same value as the * new entry) so that the new entry can fit. * - * @implNote It is recommended to create an interface (having only the {@link V} parameter) + * @implNote It is recommended to create an interface (having only the {@code } parameter) * extending this one specific to your domain and span type, then implement it using * an extension of {@link DefaultSpanMap}. See {@link ULongSpanMap} for an example. * @param the type of endpoints @@ -835,7 +835,7 @@ public interface Span> extends Comparable { if (this == obj) { return true; } - if (!(obj instanceof @SuppressWarnings("rawtypes") DefaultSpanMap that)) { + if (!(obj instanceof DefaultSpanMap that)) { return false; } if (this.domain != that.domain) { @@ -995,7 +995,7 @@ public interface Span> extends Comparable { if (this == obj) { return true; } - if (!(obj instanceof @SuppressWarnings("rawtypes") DefaultSpanSet that)) { + if (!(obj instanceof DefaultSpanSet that)) { return false; } if (!Objects.equals(this.map, that.map)) { @@ -1081,6 +1081,11 @@ public interface Span> extends Comparable { } } + /** + * Provides a default {@link #toString} implementation + * + * @return the string + */ @SuppressWarnings("unchecked") default String doToString() { return domain().toString((S) this); diff --git a/Ghidra/Test/DebuggerIntegrationTest/build.gradle b/Ghidra/Test/DebuggerIntegrationTest/build.gradle index da5acecdb7..8fbbf5e120 100644 --- a/Ghidra/Test/DebuggerIntegrationTest/build.gradle +++ b/Ghidra/Test/DebuggerIntegrationTest/build.gradle @@ -63,6 +63,7 @@ dependencies { testImplementation project(path: ':PDB', configuration: 'testArtifacts') testImplementation project(path: ':GnuDemangler', configuration: 'testArtifacts') + testImplementation project(path: ':ProposedUtils', configuration: 'testArtifacts') testImplementation project(path: ':Framework-TraceModeling', configuration: 'testArtifacts') testImplementation project(path: ':Framework-AsyncComm', configuration: 'testArtifacts') testImplementation project(path: ':Framework-Debugging', configuration: 'testArtifacts') diff --git a/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/service/breakpoint/AbstractDebuggerLogicalBreakpointServiceTest.java b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/service/breakpoint/AbstractDebuggerLogicalBreakpointServiceTest.java index 7fd2058274..bd2fbcf1ac 100644 --- a/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/service/breakpoint/AbstractDebuggerLogicalBreakpointServiceTest.java +++ b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/service/breakpoint/AbstractDebuggerLogicalBreakpointServiceTest.java @@ -1051,7 +1051,7 @@ public abstract class AbstractDebuggerLogicalBreakpointServiceTest waitForPass(() -> assertLogicalBreakpointForMappedBookmarkAnd1TraceBreakpoint(trace1)); } - @Test + @Test // Mappings are not write-behind cached public void testAbortAddMapping() throws Throwable { createTarget1(); Trace trace = getTrace(target1); @@ -1092,7 +1092,7 @@ public abstract class AbstractDebuggerLogicalBreakpointServiceTest }); } - @Test + // @Test // Not gonna with write-behind cache public void testAbortAddBreakpointAndMapping() throws Throwable { createTarget1(); Trace trace = getTrace(target1); @@ -1124,7 +1124,7 @@ public abstract class AbstractDebuggerLogicalBreakpointServiceTest assertTrue(one.getTraceBreakpoints().isEmpty()); } - @Test + @Test // Abort is on program, not trace public void testAbortAddBookmarks() throws Throwable { createTarget1(); Trace trace = getTrace(target1); @@ -1152,7 +1152,7 @@ public abstract class AbstractDebuggerLogicalBreakpointServiceTest assertTrue(breakpointService.getAllBreakpoints().isEmpty()); } - @Test + // @Test // Not gonna with write-behind cache public void testUndoRedoAddBreakpointAndMapping() throws Throwable { createTarget1(); Trace trace = getTrace(target1); @@ -1563,8 +1563,11 @@ public abstract class AbstractDebuggerLogicalBreakpointServiceTest /** * With the addition of emulated breakpoints (which are stored in the trace), this test is now * sane. + * + *

+ * With the addition of the write-behind cache, this test is no longer sane. */ - @Test + // @Test public void testAbortAddBreakpointSetSleigh() throws Throwable { DebuggerControlService controlService = addPlugin(tool, DebuggerControlServicePlugin.class); diff --git a/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiPerformanceTest.java b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiPerformanceTest.java new file mode 100644 index 0000000000..80d00e2afe --- /dev/null +++ b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiPerformanceTest.java @@ -0,0 +1,628 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.debug.service.tracermi; + +import static org.junit.Assert.assertEquals; + +import java.io.*; +import java.net.*; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.*; + +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.junit.Ignore; +import org.junit.Test; + +import db.*; +import generic.Unique; +import generic.test.rule.Repeated; +import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerIntegrationTest; +import ghidra.app.services.TraceRmiService; +import ghidra.dbg.target.schema.XmlSchemaContext; +import ghidra.debug.api.tracermi.TraceRmiAcceptor; +import ghidra.debug.api.tracermi.TraceRmiConnection; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.address.*; +import ghidra.rmi.trace.TraceRmi.*; +import ghidra.rmi.trace.TraceRmi.Compiler; +import ghidra.rmi.trace.TraceRmi.RootMessage.MsgCase; +import ghidra.trace.database.ToyDBTraceBuilder; +import ghidra.trace.database.target.DBTraceObject; +import ghidra.trace.model.Lifespan; +import ghidra.trace.model.Trace; +import ghidra.trace.model.target.TraceObject.ConflictResolution; +import ghidra.trace.model.target.TraceObjectKeyPath; +import ghidra.util.LockHold; +import ghidra.util.Msg; +import ghidra.util.database.*; +import ghidra.util.database.annot.*; +import ghidra.util.database.spatial.RStarTreeMapTest.IntRect; +import ghidra.util.database.spatial.RStarTreeMapTest.MyDomainObject; +import ghidra.util.task.TaskMonitor; + +@Ignore // Only want for manual testing +public class TraceRmiPerformanceTest extends AbstractGhidraHeadedDebuggerIntegrationTest { + public static final int REGION_COUNT = 1000; + public static final int RECORD_COUNT = 1000 * 6; + + interface Tx extends AutoCloseable { + } + + interface TraceHandle extends AutoCloseable { + Trace getTrace() throws Throwable; + + Tx startTx() throws Throwable; + + default void addLotsOfRegions() throws Throwable { + try (Tx tx = startTx()) { + AddressFactory af = getTrace().getBaseAddressFactory(); + AddressSpace space = af.getDefaultAddressSpace(); + for (int i = 0; i < REGION_COUNT; i++) { + AddressRange range = new AddressRangeImpl(space.getAddress(i * 1024), 1024); + addRegion(Integer.toString(i), range, "rw"); + } + } + } + + void addRegion(String key, AddressRange range, String perms) throws Throwable; + } + + interface TraceMaker { + TraceHandle createTrace() throws Throwable; + } + + static class ApiTx implements Tx { + private final Transaction tx; + + private ApiTx(Transaction tx) { + this.tx = tx; + } + + @Override + public void close() throws Exception { + tx.close(); + } + } + + static class ApiTraceHandle implements TraceHandle { + private final ToyDBTraceBuilder tb; + + public ApiTraceHandle(ToyDBTraceBuilder tb) { + this.tb = tb; + } + + @Override + public Trace getTrace() throws Throwable { + return tb.trace; + } + + @Override + public Tx startTx() throws Throwable { + return new ApiTx(tb.trace.openTransaction("Test")); + } + + @Override + public void addRegion(String key, AddressRange range, String perms) { + DBTraceObject region = tb.trace.getObjectManager() + .createObject(TraceObjectKeyPath.parse("Processes[0].Memory[" + key + "]")); + region.setValue(Lifespan.nowOn(0), "Range", range); + region.setValue(Lifespan.nowOn(0), "R", perms.contains("r")); + region.setValue(Lifespan.nowOn(0), "W", perms.contains("w")); + region.setValue(Lifespan.nowOn(0), "X", perms.contains("x")); + region.insert(Lifespan.nowOn(0), ConflictResolution.ADJUST); + } + + @Override + public void close() throws Exception { + tb.close(); + } + } + + class ApiTraceMaker implements TraceMaker { + @Override + public TraceHandle createTrace() throws Throwable { + tb = new ToyDBTraceBuilder(name.getMethodName(), LANGID_TOYBE64); + try (Transaction tx = tb.startTransaction()) { + tb.trace.getObjectManager().createRootObject(SCHEMA_SESSION); + } + return new ApiTraceHandle(tb); + } + } + + static class RmiTraceHandle implements TraceHandle { + private static final int RESULT_COUNT = REGION_COUNT * 6 + 2; + private final TraceRmiConnection connection; + private final Socket client; + + private final List> results = new ArrayList<>(RESULT_COUNT * 5); + private final OutputStream out; + private final InputStream in; + private final ExecutorService receiver = Executors.newSingleThreadExecutor(); + + private int seq = 1; + private int txid = 1000; + + public RmiTraceHandle(TraceRmiConnection connection, Socket client) throws IOException { + this.connection = connection; + this.client = client; + this.out = client.getOutputStream(); + this.in = client.getInputStream(); + } + + private CompletableFuture receive() { + return CompletableFuture.supplyAsync(() -> { + try { + return TraceRmiHandler.recvDelimited(in); + } + catch (IOException e) { + return ExceptionUtils.rethrow(e); + } + }, receiver); + } + + private CompletableFuture request(RootMessage msg) throws IOException { + TraceRmiHandler.sendDelimited(out, msg, seq++); + return receive(); + } + + private static void assertMsgCase(MsgCase expected, RootMessage msg) { + if (msg.getMsgCase() == MsgCase.ERROR) { + throw new AssertionError("Got RMI error: " + msg.getError().getMessage()); + } + assertEquals(expected, msg.getMsgCase()); + } + + private CompletableFuture finishNegotiate() { + return receive().thenAccept(reply -> { + assertMsgCase(MsgCase.REPLY_NEGOTIATE, reply); + }); + } + + class RmiTx implements Tx { + private final TxId txid; + + public RmiTx(TxId txid) { + this.txid = txid; + } + + @Override + public void close() throws Exception { + results.add(doEndTx(txid)); + + assertEquals(RESULT_COUNT, results.size()); + for (CompletableFuture r : results) { + r.get(1000, TimeUnit.MILLISECONDS); + } + } + } + + protected TxId nextTx() { + return TxId.newBuilder().setId(txid).build(); + } + + protected CompletableFuture doStartTx(TxId txid) throws IOException { + RootMessage msg = RootMessage.newBuilder() + .setRequestStartTx(RequestStartTx.newBuilder() + .setDescription("Test") + .setOid(DomObjId.newBuilder().setId(0)) + .setTxid(txid)) + .build(); + return request(msg).thenAccept(reply -> { + assertMsgCase(MsgCase.REPLY_START_TX, reply); + }); + } + + protected CompletableFuture doEndTx(TxId txid) throws IOException { + RootMessage msg = RootMessage.newBuilder() + .setRequestEndTx(RequestEndTx.newBuilder() + .setAbort(false) + .setOid(DomObjId.newBuilder().setId(0)) + .setTxid(txid)) + .build(); + return request(msg).thenAccept(reply -> { + assertMsgCase(MsgCase.REPLY_END_TX, reply); + }); + } + + @Override + public Tx startTx() throws Throwable { + TxId txid = nextTx(); + results.add(doStartTx(txid)); + return new RmiTx(txid); + } + + private CompletableFuture createTrace(String name) throws IOException { + RootMessage msg = RootMessage.newBuilder() + .setRequestCreateTrace(RequestCreateTrace.newBuilder() + .setPath(FilePath.newBuilder().setPath("Test/" + name)) + .setLanguage(Language.newBuilder().setId(LANGID_TOYBE64)) + .setCompiler(Compiler.newBuilder().setId("default")) + .setOid(DomObjId.newBuilder().setId(0))) + .build(); + return request(msg).thenAccept(reply -> { + assertMsgCase(MsgCase.REPLY_CREATE_TRACE, reply); + }); + } + + private CompletableFuture createRootObject() throws IOException { + RootMessage msg = RootMessage.newBuilder() + .setRequestCreateRootObject(RequestCreateRootObject.newBuilder() + .setOid(DomObjId.newBuilder().setId(0)) + .setSchemaContext(XmlSchemaContext.serialize(SCHEMA_CTX)) + .setRootSchema(SCHEMA_SESSION.getName().toString())) + .build(); + return request(msg).thenAccept(reply -> { + assertMsgCase(MsgCase.REPLY_CREATE_OBJECT, reply); + }); + } + + private CompletableFuture requestSetValue(DomObjId oid, ObjPath path, + Lifespan lifespan, String key, Object value) throws IOException { + RootMessage msg = RootMessage.newBuilder() + .setRequestSetValue(RequestSetValue.newBuilder() + .setOid(oid) + .setValue(ValSpec.newBuilder() + .setParent(ObjSpec.newBuilder() + .setPath(path)) + .setKey(key) + .setSpan(TraceRmiHandler.makeSpan(lifespan)) + .setValue(TraceRmiHandler.makeValue(value))) + .setResolution(Resolution.CR_ADJUST)) + .build(); + return request(msg).thenAccept(reply -> { + assertMsgCase(MsgCase.REPLY_SET_VALUE, reply); + }); + } + + @Override + public void addRegion(String key, AddressRange range, String perms) throws IOException { + DomObjId oid0 = DomObjId.newBuilder().setId(0).build(); + ObjPath path = ObjPath.newBuilder().setPath("Processes[0].Memory[" + key + "]").build(); + RootMessage msgCreate = RootMessage.newBuilder() + .setRequestCreateObject(RequestCreateObject.newBuilder() + .setOid(oid0) + .setPath(path)) + .build(); + results.add(request(msgCreate).thenAccept(reply -> { + assertMsgCase(MsgCase.REPLY_CREATE_OBJECT, reply); + })); + + results.add(requestSetValue(oid0, path, Lifespan.nowOn(0), "Range", range)); + results.add(requestSetValue(oid0, path, Lifespan.nowOn(0), "R", perms.contains("r"))); + results.add(requestSetValue(oid0, path, Lifespan.nowOn(0), "W", perms.contains("w"))); + results.add(requestSetValue(oid0, path, Lifespan.nowOn(0), "X", perms.contains("x"))); + + RootMessage msgInsert = RootMessage.newBuilder() + .setRequestInsertObject(RequestInsertObject.newBuilder() + .setOid(oid0) + .setObject(ObjSpec.newBuilder().setPath(path)) + .setSpan(TraceRmiHandler.makeSpan(Lifespan.nowOn(0))) + .setResolution(Resolution.CR_ADJUST)) + .build(); + results.add(request(msgInsert).thenAccept(reply -> { + assertMsgCase(MsgCase.REPLY_INSERT_OBJECT, reply); + })); + } + + @Override + public Trace getTrace() throws Throwable { + return Unique.assertOne(connection.getTargets()).getTrace(); + } + + @Override + public void close() throws Exception { + client.close(); + connection.close(); + } + } + + class RmiTraceMaker implements TraceMaker { + protected TraceRmiService getService() throws Throwable { + TraceRmiService service = tool.getService(TraceRmiService.class); + if (service != null) { + return service; + } + return addPlugin(tool, TraceRmiPlugin.class); + } + + private void startNegotiate(OutputStream out) throws IOException { + RootMessage msg = RootMessage.newBuilder() + .setRequestNegotiate(RequestNegotiate.newBuilder() + .setDescription("Test") + .setVersion(TraceRmiHandler.VERSION) + .addAllMethods(List.of())) + .build(); + TraceRmiHandler.sendDelimited(out, msg, 0); + } + + @Override + public TraceHandle createTrace() throws Throwable { + TraceRmiService service = getService(); + TraceRmiAcceptor acceptor = service.acceptOne(new InetSocketAddress("localhost", 0)); + Socket client = new Socket(); + + client.connect(acceptor.getAddress()); + startNegotiate(client.getOutputStream()); + TraceRmiConnection connection = acceptor.accept(); + RmiTraceHandle th = new RmiTraceHandle(connection, client); + th.finishNegotiate(); + + TxId txid = th.nextTx(); + th.createTrace(name.getMethodName()).get(10000, TimeUnit.MILLISECONDS); + th.doStartTx(txid).get(1000, TimeUnit.MILLISECONDS); + th.createRootObject().get(1000, TimeUnit.MILLISECONDS); + th.doEndTx(txid).get(1000, TimeUnit.MILLISECONDS); + tb = new ToyDBTraceBuilder(th.getTrace()); + return th; + } + } + + class NerfedRmiTraceMaker extends RmiTraceMaker { + @Override + protected TraceRmiService getService() throws Throwable { + tool.removePlugins(tool.getManagedPlugins() + .stream() + .filter(p -> p instanceof TraceRmiPlugin) + .toList()); + return addPlugin(tool, NerfedTraceRmiPlugin.class); + } + } + + static class TimeMe implements AutoCloseable { + private final String name; + private final long start; + + public TimeMe(String name) { + this.name = name; + Msg.info(this, "Starting: " + name); + this.start = System.currentTimeMillis(); + } + + @Override + public void close() throws Exception { + long elapsed = System.currentTimeMillis() - start; + Msg.info(this, "Finished: " + name + ". Took " + elapsed + " ms"); + } + } + + @Test + @Repeated(20) // First time will measure warm-up + public void testMeasureViaApiIsolated() throws Throwable { + TraceMaker tm = new ApiTraceMaker(); + try (TraceHandle th = tm.createTrace()) { + try (TimeMe __ = new TimeMe("Add via API. Isolated.")) { + th.addLotsOfRegions(); + } + } + } + + @Test + @Repeated(2) // First time will measure warm-up + public void testMeasureViaRmiIsolated() throws Throwable { + TraceMaker tm = new RmiTraceMaker(); + try (TraceHandle th = tm.createTrace()) { + try (TimeMe __ = new TimeMe("Add via RMI. Isolated.")) { + th.addLotsOfRegions(); + } + } + } + + static class NerfedTraceRmiHandler extends TraceRmiHandler { + public NerfedTraceRmiHandler(TraceRmiPlugin plugin, Socket socket) throws IOException { + super(plugin, socket); + } + + @Override + protected ReplyCreateObject handleCreateObject(RequestCreateObject req) { + return ReplyCreateObject.getDefaultInstance(); + } + + @Override + protected ReplySetValue handleSetValue(RequestSetValue req) + throws AddressOverflowException { + return ReplySetValue.getDefaultInstance(); + } + + @Override + protected ReplyInsertObject handleInsertObject(RequestInsertObject req) { + return ReplyInsertObject.getDefaultInstance(); + } + } + + static class NerfedTraceRmiAcceptor extends DefaultTraceRmiAcceptor { + public NerfedTraceRmiAcceptor(TraceRmiPlugin plugin, SocketAddress address) { + super(plugin, address); + } + + @Override + public NerfedTraceRmiHandler doAccept(TraceRmiAcceptor acceptor) + throws IOException { + Socket client = socket.accept(); + NerfedTraceRmiHandler handler = new NerfedTraceRmiHandler(plugin, client); + handler.start(); + plugin.listeners.invoke().connected(handler, getConnectMode(), acceptor); + return handler; + } + } + + public static class NerfedTraceRmiPlugin extends TraceRmiPlugin { + public NerfedTraceRmiPlugin(PluginTool tool) { + super(tool); + } + + @Override + public DefaultTraceRmiAcceptor acceptOne(SocketAddress address) throws IOException { + NerfedTraceRmiAcceptor acceptor = new NerfedTraceRmiAcceptor(this, address); + acceptor.start(); + listeners.invoke().waitingAccept(acceptor); + return acceptor; + } + } + + @Test + @Repeated(2) + public void testMeasureRmiNetworkOnly() throws Throwable { + TraceMaker tm = new NerfedRmiTraceMaker(); + try (TraceHandle th = tm.createTrace()) { + try (TimeMe __ = new TimeMe("Add via RMI. Isolated.")) { + th.addLotsOfRegions(); + } + } + } + + @Test + @Repeated(2) + public void testMeasurePlainTable() throws Exception { + DBHandle handle = new DBHandle(); + // Some comparable number of fields to a DBTraceObjectValue + Schema schema = new SchemaBuilder() + .field("Parent", LongField.class) + .field("EntryKey", StringField.class) + .field("MinSnap", LongField.class) + .field("MaxSnap", LongField.class) + .field("Value", BinaryField.class) + .build(); + + try (TimeMe tm = new TimeMe("Plain table")) { + try (Transaction tx = handle.openTransaction(null)) { + Table table = handle.createTable("Test", schema); + for (int i = 0; i < RECORD_COUNT; i++) { + DBRecord rec = schema.createRecord(0); + rec.setLongValue(0, 0); + rec.setString(1, "Whatever"); + rec.setLongValue(2, 0); + rec.setLongValue(3, Long.MAX_VALUE); + + ByteBuffer rangeEnc = ByteBuffer.allocate(18); + rangeEnc.putShort((short) 0x204); // Made up "space id" + rangeEnc.putLong(1024 * i); + rangeEnc.putLong(1024 * i + 1023); + rec.setBinaryData(4, rangeEnc.array()); + + table.putRecord(rec); + } + } + } + } + + @DBAnnotatedObjectInfo(version = 0) + public static class TestObject extends DBAnnotatedObject { + + static final String NAME_PARENT = "Parent"; + static final String NAME_ENTRY_KEY = "EntryKey"; + static final String NAME_MIN_SNAP = "MinSnap"; + static final String NAME_MAX_SNAP = "MaxSnap"; + static final String NAME_VALUE = "Value"; + + @DBAnnotatedColumn(NAME_PARENT) + static DBObjectColumn COL_PARENT; + @DBAnnotatedColumn(NAME_ENTRY_KEY) + static DBObjectColumn COL_ENTRY_KEY; + @DBAnnotatedColumn(NAME_MIN_SNAP) + static DBObjectColumn COL_MIN_SNAP; + @DBAnnotatedColumn(NAME_MAX_SNAP) + static DBObjectColumn COL_MAX_SNAP; + @DBAnnotatedColumn(NAME_VALUE) + static DBObjectColumn COL_VALUE; + + @DBAnnotatedField(column = NAME_PARENT) + private long parent; + @DBAnnotatedField(column = NAME_ENTRY_KEY) + private String entryKey; + @DBAnnotatedField(column = NAME_MIN_SNAP) + private long minSnap; + @DBAnnotatedField(column = NAME_MAX_SNAP) + private long maxSnap; + @DBAnnotatedField(column = NAME_VALUE) + private byte[] value; + + protected TestObject(DBCachedObjectStore store, DBRecord record) { + super(store, record); + } + + public void set(long parent, String entryKey, long minSnap, long maxSnap, byte[] value) { + this.parent = parent; + this.entryKey = entryKey; + this.minSnap = minSnap; + this.maxSnap = maxSnap; + this.value = value; + + update(COL_PARENT, COL_ENTRY_KEY, COL_MIN_SNAP, COL_MAX_SNAP, COL_VALUE); + } + } + + public static class TestDomainObject extends DBCachedDomainObjectAdapter { + protected TestDomainObject(DBHandle dbh, DBOpenMode openMode, TaskMonitor monitor, + String name, int timeInterval, int bufSize, Object consumer) { + super(dbh, openMode, monitor, name, timeInterval, bufSize, consumer); + } + + @Override + public boolean isChangeable() { + return true; + } + + @Override + public String getDescription() { + return "Test Domain Object"; + } + } + + @Test + @Repeated(2) + public void testMeasureObjectStore() throws Exception { + DBHandle handle = new DBHandle(); + TestDomainObject domObj = + new TestDomainObject(handle, DBOpenMode.CREATE, monitor, "Test", 500, 1000, this); + DBCachedObjectStoreFactory factory = new DBCachedObjectStoreFactory(domObj); + + try (TimeMe tm = new TimeMe("Object Store")) { + try (Transaction tx = domObj.openTransaction("Test")) { + DBCachedObjectStore store = factory.getOrCreateCachedStore("Test", + TestObject.class, TestObject::new, false); + for (int i = 0; i < RECORD_COUNT; i++) { + try (LockHold hold = LockHold.lock(domObj.getReadWriteLock().writeLock())) { + TestObject obj = store.create(); + + ByteBuffer rangeEnc = ByteBuffer.allocate(18); + rangeEnc.putShort((short) 0x204); // Made up "space id" + rangeEnc.putLong(1024 * i); + rangeEnc.putLong(1024 * i + 1023); + obj.set(0, "Whatever", 0, Long.MAX_VALUE, rangeEnc.array()); + } + } + } + } + } + + @Test + @Repeated(20) + public void testMeasureRTree() throws Exception { + MyDomainObject domObj = new MyDomainObject(this); + + try (TimeMe tm = new TimeMe("Object Store")) { + try (Transaction tx = domObj.openTransaction("Test")) { + for (int i = 0; i < RECORD_COUNT; i++) { + domObj.map.put( + IntRect.ALL.immutable(0, Integer.MAX_VALUE, i * 1024, i * 1024 + 1023), + "Whatever"); + } + } + } + } +}