diff --git a/native/src/com/android/commands/monkey/ape/agent/StatefulAgent.java b/native/src/com/android/commands/monkey/ape/agent/StatefulAgent.java index ffffb2b..0c302bd 100644 --- a/native/src/com/android/commands/monkey/ape/agent/StatefulAgent.java +++ b/native/src/com/android/commands/monkey/ape/agent/StatefulAgent.java @@ -118,6 +118,9 @@ public abstract class StatefulAgent extends ApeAgent implements GraphListener { boolean regionShadowAudit = com.android.commands.monkey.ape.utils.Config.getBoolean("ape.jev.region_shadow_audit", false); if (jevEnabled || telemetry || regionShadowAudit || com.android.commands.monkey.ape.utils.ApeSnapshot.enabled() || com.android.commands.monkey.ape.utils.Config.getBoolean("ape.jev.region_locator_shadow",false)) { this.jev = com.android.commands.monkey.ape.jev.JevRuntime.create(ape.getOutputDirectory(), ape, !jevEnabled); + } else if (com.android.commands.monkey.ape.utils.Config.getBoolean("ape.jev.flow_shadow",false)) { + try { this.jev = com.android.commands.monkey.ape.jev.JevRuntime.create(ape.getOutputDirectory(), ape, true); } + catch (Throwable error) { System.err.println("FLOW_SHADOW_RUNTIME_DISABLED " + error.getClass().getSimpleName()); } } } diff --git a/native/src/com/android/commands/monkey/ape/jev/JevEvidenceAudit.java b/native/src/com/android/commands/monkey/ape/jev/JevEvidenceAudit.java index 83647ba..7f4518a 100644 --- a/native/src/com/android/commands/monkey/ape/jev/JevEvidenceAudit.java +++ b/native/src/com/android/commands/monkey/ape/jev/JevEvidenceAudit.java @@ -215,6 +215,13 @@ final class JevEvidenceAudit implements AutoCloseable { synchronized(recent){recent.addLast(value);while(recent.size()>32)recent.removeFirst();} }catch(Exception error){failures.incrementAndGet();} } + /** Read an already-frozen input only. Never resolves a node or recomputes identity. */ + JSONObject flowInputReference(GUITreeAction action) { + try { + JSONObject input=inputs.get(action);if(input==null)return null; + return JevRegistry.fields("audit_input_id",input.opt("input_id"),"concrete",input.opt("concrete")); + } catch(Throwable ignored){return null;} + } void input(GUITreeAction action,long step,long version,String phase,int total,int success,int failed,int discarded) { try { JSONObject source=inputs.get(action);JSONObject value=stamp("input_lifecycle",step,version); diff --git a/native/src/com/android/commands/monkey/ape/jev/JevFlowShadow.java b/native/src/com/android/commands/monkey/ape/jev/JevFlowShadow.java new file mode 100644 index 0000000..7f1bf8f --- /dev/null +++ b/native/src/com/android/commands/monkey/ape/jev/JevFlowShadow.java @@ -0,0 +1,105 @@ +package com.android.commands.monkey.ape.jev; + +import java.io.*; +import java.util.*; +import java.util.concurrent.*; +import org.json.*; +import com.android.commands.monkey.ape.model.*; +import com.android.commands.monkey.ape.tree.*; +import com.android.commands.monkey.ape.utils.Config; + +/** Optional passive journal. Never calls selection, resolution, Naming, RNG, gates or providers. */ +public final class JevFlowShadow implements Closeable { + private final String runId; + private final ArrayBlockingQueue queue; + private final Thread worker; + private final File file; + private volatile boolean closing,failed; + private long seq,inputSeq,offered,dropped; private int peak; + private final IdentityHashMap inputs=new IdentityHashMap(); + private final Set emittedInputIds=new HashSet(); + private final Set transitionIds=new HashSet(); + private final Set states=new HashSet(),triples=new HashSet(); + private static Object value(Object v){return v==null?JSONObject.NULL:v;} + static JSONObject fields(Object... kv) throws JSONException {JSONObject o=new JSONObject();for(int i=0;i(capacity); + final Writer out=new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file),"UTF-8")); + worker=new Thread(new Runnable(){public void run(){try{while(!closing || !queue.isEmpty()){ + String s=queue.poll(100,TimeUnit.MILLISECONDS);if(s!=null){out.write(s);out.write('\n');out.flush();} + }}catch(Throwable error){failed=true;}finally{try{out.close();}catch(IOException error){failed=true;}}}},"region-flow-shadow-writer"); + worker.setDaemon(true);worker.start(); + } + private synchronized void emit(String kind,JSONObject facts) { + if(closing || failed)return; + try { + facts.put("schema",1).put("run_id",runId).put("event_seq",++seq).put("event_kind",kind) + .put("timestamp",System.currentTimeMillis()).put("monotonic_ns",System.nanoTime()) + .put("dropped_before",dropped).put("execution_confirmed",facts.optBoolean("execution_confirmed",false)); + offered++;if(!queue.offer(facts.toString()))dropped++;peak=Math.max(peak,queue.size()); + }catch(Throwable error){failed=true;} + } + public void selection(ModelAction action,int step,String strategy) { + try{emit("SELECTION",fields("step",step,"source_state",action.getState().getGraphId(),"source_activity",action.getState().getActivity(), + "model_action_identity",action.getGraphId(),"action_type",action.getType().name(),"strategy",strategy));}catch(Throwable error){failed=true;} + } + public void beforeInput(Action action,long step) { + try { + if(action.getType().canStartApp()){inputs.clear();emit(action.getType()==ActionType.EVENT_CLEAN_RESTART?"RESET":"RESTART",fields("step",step,"action_type",action.getType().name(),"is_restart",true,"boundary_stage","input_requested_not_execution_proof"));return;} + if(!(action instanceof ModelAction))return; + ModelAction a=(ModelAction)action;GUITreeAction concrete=a.getResolvedGUITreeAction();if(concrete==null)return; + JSONObject progress=fields("selected_model_action_visits",a.getVisitedCount(),"selected_model_action_saturated",a.isSaturated(),"selected_model_action_unvisited",a.isUnvisited(),"stage","before_input"); + JSONObject input=fields("input_id","flow-input-"+(++inputSeq),"source_state",a.getState().getGraphId(),"source_activity",a.getState().getActivity(), + "model_action_identity",a.getGraphId(),"action_type",a.getType().name(),"step",step,"model_progress",progress); + inputs.put(concrete,input);emit("INPUT_REQUESTED",new JSONObject(input.toString())); + }catch(Throwable error){failed=true;} + } + public void cachedInput(GUITreeAction action,JSONObject cached) { + try { + JSONObject in=inputs.get(action);if(in==null || cached==null)return; + in.put("audit_input_id",cached.opt("audit_input_id"));JSONObject c=cached.optJSONObject("concrete"); + if(c!=null){in.put("concrete_identity",c.opt("identity_digest"));in.put("concrete_full_name",c.opt("concrete_identity")); + in.put("control",new JSONObject(c.toString()));} + }catch(Throwable error){failed=true;} + } + public void generated(GUITreeAction action,int total) { + try{JSONObject in=inputs.get(action);emit("INPUT_GENERATED",fields("input_id",in==null?null:in.opt("input_id"),"event_count",total));}catch(Throwable error){failed=true;} + } + public void inputCompleted(GUITreeAction action,boolean confirmed,int total,int success,int failure,int discarded) { + try{JSONObject in=inputs.get(action);if(in!=null)in.put("input_confirmed",confirmed);emit("INPUT_COMPLETED",fields("input_id",in==null?null:in.opt("input_id"),"execution_confirmed",confirmed, + "event_count",total,"injected_success",success,"injected_failed",failure,"discarded",discarded));}catch(Throwable error){failed=true;} + } + public void boundary(String reason) {try{inputs.clear();emit("TRACE_BOUNDARY",fields("reason",reason,"is_restart",JSONObject.NULL));}catch(Throwable error){failed=true;}} + public void transition(GUITreeTransition transition,String id,boolean inputConfirmed,boolean rebuilding,boolean nativeNewState,boolean nativeNewAction) { + try { + if(transitionIds.contains(id))return;transitionIds.add(id); + ModelAction a=transition.getAction().getModelAction();State source=transition.getSource().getCurrentState(),target=transition.getTarget().getCurrentState(); + String u=source.getGraphId(),v=target.getGraphId(),triple=u+"\u0000"+a.getGraphId()+"\u0000"+v; + JSONObject in=inputs.remove(transition.getAction()); + boolean live=inputConfirmed && in!=null && in.optBoolean("input_confirmed",false) && emittedInputIds.add(in.getString("input_id")); + JSONObject row=fields("transition_identity",id,"native_transition_identity",transition.getCurrentStateTransition().getGraphId(), + "source_state",u,"target_state",v,"source_activity",source.getActivity(),"target_activity",target.getActivity(), + "model_action_identity",a.getGraphId(),"action_type",a.getType().name(),"execution_confirmed",live, + "native_input_confirmed",inputConfirmed,"history_rebuild",rebuilding && in==null,"model_rebinding_pass",rebuilding,"input_id",in==null?null:in.opt("input_id"), + "is_back",a.getType()==ActionType.MODEL_BACK,"is_restart",false,"is_new_state",live?!states.contains(v):JSONObject.NULL, + "is_new_transition",live?!triples.contains(triple):JSONObject.NULL,"native_new_state",nativeNewState,"native_new_model_action",nativeNewAction, + "model_progress",in==null?null:in.opt("model_progress"),"source_gui_id",transition.getSource().getId(),"target_gui_id",transition.getTarget().getId(), + "source_gui_timestamp",transition.getSource().getTimestamp(),"target_gui_timestamp",transition.getTarget().getTimestamp(), + "audit_input_id",in==null?null:in.opt("audit_input_id"),"concrete_identity",in==null?null:in.opt("concrete_identity"), + "control",in==null?null:in.opt("control"),"concrete_identity_status","cached existing audit input only; no new Naming/identity computation"); + if(live){states.add(u);states.add(v);triples.add(triple);}emit("TRANSITION",row); + }catch(Throwable error){failed=true;} + } + public void close() { + closing=true;try{worker.join(3000);}catch(InterruptedException e){Thread.currentThread().interrupt();} + try {Writer out=new OutputStreamWriter(new FileOutputStream(new File(file.getParentFile(),"region-flow-status.json")),"UTF-8"); + out.write(fields("run_id",runId,"offered",offered,"dropped",dropped,"queue_peak",peak,"failed",failed,"writer_alive",worker.isAlive()).toString());out.close(); + }catch(Throwable ignored){} + } +} diff --git a/native/src/com/android/commands/monkey/ape/jev/JevRuntime.java b/native/src/com/android/commands/monkey/ape/jev/JevRuntime.java index 39067c6..7daa6c7 100644 --- a/native/src/com/android/commands/monkey/ape/jev/JevRuntime.java +++ b/native/src/com/android/commands/monkey/ape/jev/JevRuntime.java @@ -22,6 +22,7 @@ public final class JevRuntime { private final JevTiming timing=new JevTiming(); private final JevOutput output=new JevOutput(timing); private final JevEvents events; + private final JevFlowShadow flowShadow; private final JevRegistry registry; private JevTransport probeTransport; private JevSemanticReview semanticReview; @@ -105,6 +106,7 @@ public final class JevRuntime { private JevRuntime(File directory,String endpoint,String token,int timeoutMs,int maxBytes,String model,MonkeySourceApe ape,boolean observeOnly) throws IOException { boolean configuredShadowAudit=Config.getBoolean("ape.jev.region_shadow_audit", false) || Config.getBoolean("ape.jev.region_locator_shadow",false); JevRegion.validateConfiguration(!observeOnly || configuredShadowAudit); + flowShadow=JevFlowShadow.open(directory,runId); receipts=new File(directory,"jev-receipts");receipts.mkdirs();graphSnapshotFile=new File(directory,"jev-graph.json"); screenshotDirectory=new File(directory,"ape-state-screenshots");this.ape=ape; observeOnly |= Config.getBoolean("ape.jev.region_locator_shadow",false) @@ -206,7 +208,7 @@ public final class JevRuntime { void receiptFailed(Exception error){decisionHealthy=false;events.emit("RECEIPT_FAILED",step,registry.version(),null,null,JevRegistry.fields("reason","receipt_write_failed","error",error.getClass().getSimpleName()));} public void regionRoot(State state){if(region!=null)region.root(state);} public void beforeSelection(Model model,State state,GUITree tree,int timestamp){if(region!=null)region.safePoint(model,state,tree,timestamp);} - public void shadowSelection(ModelAction action,GUITree tree,int timestamp,String strategy){if(region!=null && regionShadowAudit)region.auditSelection(action,tree,timestamp,strategy);} + public void shadowSelection(ModelAction action,GUITree tree,int timestamp,String strategy){if(flowShadow!=null)flowShadow.selection(action,timestamp,strategy);if(region!=null && regionShadowAudit)region.auditSelection(action,tree,timestamp,strategy);} JevRegion.Fact explorationFact(State state) { GUITree tree=state.getLatestGUITree();boolean known=tree!=null && tree.getCurrentNames()!=null; boolean saturated=true; @@ -242,6 +244,7 @@ public final class JevRuntime { boolean loop=source==target; boolean guiUnchanged=JevEvidence.guiIdentity(transition.getSource()).equals(JevEvidence.guiIdentity(transition.getTarget())); boolean inputConfirmed=confirmedInputs.contains(transition.getAction()); + if(fresh && flowShadow!=null)flowShadow.transition(transition,id,inputConfirmed,rebuilding,newState,newAction); JSONObject probeTargetScreenshot=null; if(fresh && !rebuilding && inputConfirmed && (probe || semanticProbe)) probeTargetScreenshot=captureProbeScreenshot(transition.getTarget(),null,null,"target"); @@ -699,6 +702,7 @@ public final class JevRuntime { if(action instanceof ModelAction)evidenceAudit.directText(((ModelAction)action).getResolvedGUITreeAction(),text,sent,step,registry.version()); } public void beforeInput(Action action){ + if(flowShadow!=null)flowShadow.beforeInput(action,step); if(!(action instanceof ModelAction))evidenceAudit.beforeOtherInput(action,step,registry.version()); boolean auditProbe=action instanceof ModelAction && (selected!=null || semanticSelected) && ((ModelAction)action).getResolvedGUITreeAction()==selectedAction; @@ -709,6 +713,10 @@ public final class JevRuntime { if(action instanceof ModelAction)evidenceAudit.beforeInput(ape,(ModelAction)action,step,registry.version(),auditProbe, auditProbe?probeSourceScreenshot:null,auditProbe?(selected!=null?selected.id:semanticEntryId):null, auditProbe?(selected!=null?selected.requestId:semanticRequestId):null,auditProbe?semanticCandidateId:null); + if(flowShadow!=null && action instanceof ModelAction) { + GUITreeAction concrete=((ModelAction)action).getResolvedGUITreeAction(); + flowShadow.cachedInput(concrete,evidenceAudit.flowInputReference(concrete)); + } if(regionContextAudit && action instanceof ModelAction) { ModelAction modelAction=(ModelAction)action;GUITreeAction concrete=modelAction.getResolvedGUITreeAction(); GUITree tree=modelAction.getResolvedGUITree();GUITreeNode node=modelAction.getResolvedNode(); @@ -747,6 +755,7 @@ public final class JevRuntime { if(trace.entry!=null){selectedInput=trace;entryEvent("PROBE_INPUT_GENERATED",trace.entry,JevRegistry.fields("event_count",trace.total));} else if(trace.semanticEntryId!=null){selectedInput=trace;events.emit("PROBE_INPUT_GENERATED",step,registry.version(),null,null,JevRegistry.fields( "entry_id",trace.semanticEntryId,"request_id",trace.semanticRequestId,"candidate_id",trace.semanticCandidateId,"event_count",trace.total));} + if(flowShadow!=null)flowShadow.generated(trace.action,trace.total); evidenceAudit.input(trace.action,step,registry.version(),"generated",trace.total,0,0,0); if(trace.total==0)inputSummary(trace); } @@ -763,6 +772,7 @@ public final class JevRuntime { if(trace.remaining()==0)inputSummary(trace); } private void inputSummary(InputTrace trace) { + if(flowShadow!=null)flowShadow.inputCompleted(trace.action,trace.confirmed(),trace.total,trace.success,trace.failed,trace.discarded); evidenceAudit.input(trace.action,step,registry.version(),trace.phase(),trace.total,trace.success,trace.failed,trace.discarded); boolean added=false; if(trace.confirmed()){ @@ -790,6 +800,7 @@ public final class JevRuntime { "entry_id",trace.semanticEntryId,"request_id",trace.semanticRequestId,"candidate_id",trace.semanticCandidateId))); } public void selectionFailed(String reason) { + if(flowShadow!=null)flowShadow.boundary(reason); if(region!=null)region.interrupted(reason); if(selected==null && !semanticSelected)return; probeSourceScreenshot=null; @@ -833,6 +844,7 @@ public final class JevRuntime { if(regionTransport!=null)regionTransport.close(); for(InputTrace trace:new HashSet(inputEvents.values()))inputSummary(trace);inputEvents.clear(); output.drain();recordFinalStateTelemetry();writeGraphSnapshot();registry.finishAccounting(step);registry.counters(step); + if(flowShadow!=null)flowShadow.close(); output.drain();closed=true;timing.count("output_queue_peak",output.peak()); try{output.save(new File(graphSnapshotFile.getParentFile(),"jev-performance.json"),timing.snapshot(),true);}catch(IOException error){output.fail(error);} events.emit("RUN_FINISHED",step,registry.version(),null,null,JevRegistry.fields("run_id",runId,"jev_enabled",!observeOnly,