diff --git a/src/hypofuzz/dashboard/dashboard.py b/src/hypofuzz/dashboard/dashboard.py index 8ed2acb9..55965b39 100644 --- a/src/hypofuzz/dashboard/dashboard.py +++ b/src/hypofuzz/dashboard/dashboard.py @@ -208,7 +208,8 @@ async def initial(self, tests: dict[str, Test]) -> None: } await self.send_event(report_event) - # then its observations. + # then its observations, sending the observations (without their repr) + # first, followed by the repr of each observation. for obs_type, observations in [ ("rolling", test.rolling_observations), ("corpus", test.corpus_observations), @@ -224,6 +225,26 @@ async def initial(self, tests: dict[str, Test]) -> None: }, ) + for obs_type, observations in [ + ("rolling", test.rolling_observations), + ("corpus", test.corpus_observations), + ]: + await self.send_event( + { + "type": DashboardEventType.SET_OBSERVATION_REPRS, + "nodeid": self.nodeid, + "observation_type": obs_type, # type: ignore + "representations": [ + { + # using run_start as the observation primary key + "run_start": obs.run_start, + "repr": obs.representation, + } + for obs in observations + ], + }, + ) + async def on_event( self, event_type: Literal["save", "delete"], key: DatabaseEventKey, value: Any ) -> None: diff --git a/src/hypofuzz/dashboard/models.py b/src/hypofuzz/dashboard/models.py index f759c809..54ced898 100644 --- a/src/hypofuzz/dashboard/models.py +++ b/src/hypofuzz/dashboard/models.py @@ -14,7 +14,7 @@ class DashboardObservation(TypedDict): type: str status: ObservationStatus status_reason: str - representation: str + representation: None arguments: dict[str, Any] how_generated: str features: dict[str, Any] @@ -29,7 +29,8 @@ def dashboard_observation(observation: Observation) -> DashboardObservation: "type": observation.type, "status": observation.status, "status_reason": observation.status_reason, - "representation": observation.representation, + # explicitly dropping representation + "representation": None, "arguments": observation.arguments, "how_generated": observation.how_generated, "features": observation.features, @@ -69,7 +70,8 @@ class DashboardEventType(IntEnum): ADD_TESTS = 1 ADD_REPORTS = 2 ADD_OBSERVATIONS = 3 - SET_FAILURE = 4 + SET_OBSERVATION_REPRS = 4 + SET_FAILURE = 5 ObservationType = Literal["rolling", "corpus"] @@ -101,6 +103,15 @@ class AddObservationsEvent(TypedDict): observations: list[DashboardObservation] +class SetObservationReprsEvent(TypedDict): + type: Literal[DashboardEventType.SET_OBSERVATION_REPRS] + nodeid: str + # TODO we might want to make this names shorter (obs_type / reprs) for json + # size + observation_type: ObservationType + representations: list[dict[str, Any]] + + class SetFailureEvent(TypedDict): type: Literal[DashboardEventType.SET_FAILURE] failure: DashboardObservation @@ -110,5 +121,6 @@ class SetFailureEvent(TypedDict): AddTestsEvent, AddReportsEvent, AddObservationsEvent, + SetObservationReprsEvent, SetFailureEvent, ] diff --git a/src/hypofuzz/frontend/src/context/DataProvider.tsx b/src/hypofuzz/frontend/src/context/DataProvider.tsx index f324a52a..18e986b9 100644 --- a/src/hypofuzz/frontend/src/context/DataProvider.tsx +++ b/src/hypofuzz/frontend/src/context/DataProvider.tsx @@ -23,7 +23,8 @@ enum DashboardEventType { ADD_TESTS = 1, ADD_REPORTS = 2, ADD_OBSERVATIONS = 3, - SET_FAILURE = 4, + SET_OBSERVATION_REPRS = 4, + SET_FAILURE = 5, } type TestsAction = @@ -48,6 +49,12 @@ type TestsAction = observation_type: "rolling" | "corpus" observations: Observation[] } + | { + type: DashboardEventType.SET_OBSERVATION_REPRS + nodeid: string + observation_type: "rolling" | "corpus" + representations: { run_start: number; representation: string }[] + } function testsReducer( state: Map, @@ -110,6 +117,20 @@ function testsReducer( return newState } + case DashboardEventType.SET_OBSERVATION_REPRS: { + const { nodeid, observation_type, representations } = action + const test = getOrCreateTest(nodeid) + const observations = + observation_type === "rolling" + ? test.rolling_observations + : test.corpus_observations + for (const { run_start, representation } of representations) { + const observation = observations.find(o => o.run_start === run_start)! + observation.representation = representation + } + return newState + } + default: throw new Error("non-exhaustive switch in testsReducer") } @@ -250,6 +271,19 @@ export function DataProvider({ children }: DataProviderProps) { break } + case DashboardEventType.SET_OBSERVATION_REPRS: { + dispatch({ + type: DashboardEventType.SET_OBSERVATION_REPRS, + nodeid: data.nodeid, + observation_type: data.observation_type, + representations: data.representations.map((r: any) => ({ + run_start: Number(r.run_start), + representation: r.repr, + })), + }) + break + } + default: throw new Error(`Unknown event type: ${data.type}`) } diff --git a/src/hypofuzz/frontend/src/tyche/Representation.tsx b/src/hypofuzz/frontend/src/tyche/Representation.tsx index 80ebc581..daebb3b7 100644 --- a/src/hypofuzz/frontend/src/tyche/Representation.tsx +++ b/src/hypofuzz/frontend/src/tyche/Representation.tsx @@ -52,6 +52,9 @@ export function Representation({ observations, observationType }: Props) { const rawRepresentations = new Map() observations.forEach(observation => { const repr = observation.representation + if (repr === null) { + return + } rawRepresentations.set(repr, (rawRepresentations.get(repr) || 0) + 1) }) diff --git a/src/hypofuzz/frontend/src/tyche/Samples.tsx b/src/hypofuzz/frontend/src/tyche/Samples.tsx index 1fa36293..ce86befb 100644 --- a/src/hypofuzz/frontend/src/tyche/Samples.tsx +++ b/src/hypofuzz/frontend/src/tyche/Samples.tsx @@ -15,16 +15,25 @@ export function Samples({ observations }: { observations: Observation[] }) { // unique observation for that representation. const uniqueReprIndex = new Map() for (const [index, observation] of observations.entries()) { + if (observation.representation === null) { + continue + } uniqueReprIndex.set(observation.representation, index) } function isUnique(observation: Observation) { + if (observation.representation === null) { + return false + } return ( observations.indexOf(observation) === uniqueReprIndex.get(observation.representation) ) } function isDuplicate(observation: Observation) { + if (observation.representation === null) { + return false + } return ( observations.indexOf(observation) !== uniqueReprIndex.get(observation.representation) diff --git a/src/hypofuzz/frontend/src/types/dashboard.ts b/src/hypofuzz/frontend/src/types/dashboard.ts index a3f07609..e3d6b3b6 100644 --- a/src/hypofuzz/frontend/src/types/dashboard.ts +++ b/src/hypofuzz/frontend/src/types/dashboard.ts @@ -162,7 +162,7 @@ export class Observation extends Dataclass { public type: string, public status: ObservationStatus, public status_reason: string, - public representation: string, + public representation: string | null, // arguments is a reserved keyword in javascript public arguments_: Map, public how_generated: string, diff --git a/src/hypofuzz/hypofuzz.py b/src/hypofuzz/hypofuzz.py index 3a5efce0..e41a8fb3 100644 --- a/src/hypofuzz/hypofuzz.py +++ b/src/hypofuzz/hypofuzz.py @@ -478,6 +478,8 @@ def callback(test_case: dict) -> None: # re-use per FuzzProcess. Overwrite with the current timestamp for use # in sorting observations. This is not perfectly reliable in a # distributed setting, but is good enough. + # + # We also re-use this as the primary key for this observation. test_case["run_start"] = time.time() # "arguments" duplicates part of the call repr in "representation". # We don't use this for anything, so drop it.