Skip to content

JSON and Logs

Jordan Samhi edited this page Oct 23, 2026 · 1 revision

Use JSON for scripts and retain the runtime log to reproduce or investigate a report. The HTML report additionally embeds reconstructed code for interactive browsing.

JSON coverage fields

Aggregate output contains coverageSummary. -pam and capture also include perMinuteSummaries. Only selected coverage categories appear.

This illustrative coverage-only report shows the field names and value types:

{
  "coverageSummary": {
    "methods": {"covered": 3, "total": 5, "percent": "60.0%"}
  },
  "perMinuteSummaries": {
    "10-23 09:30": {
      "methods": {"cumulative": 2, "overall": 5, "percent": "40.0%"}
    },
    "10-23 09:31": {
      "methods": {"cumulative": 3, "overall": 5, "percent": "60.0%"}
    }
  }
}

Aggregate metrics use covered and total. Timeline metrics use cumulative and overall. Percentages are strings with a percent sign; for calculations, use the integer counts. For a zero denominator, JSON preserves "0.0%", while the terminal displays n/a.

The category keys are statements, methods, classes, activities, services, broadcast-receivers, and content-providers. A graph-only run can have an empty coverage summary.

Dynamic call graph schema

When call tracing is requested or matching call records are available, callGraph contains:

Field Meaning
available Whether matching call observations were accepted
nodes Method signatures and display metadata
edges Aggregated caller-to-callee relationships
stats Record, callsite, and edge counts
launcherActivities Manifest-derived launcher target classes; may be absent in older reports or null when not collected
note Text describing observation semantics and limits

A node has id (full method signature), className, label, and inReport. inReport means a method body is available in the collected code listing. It is not a coverage flag. Code bodies are collected for HTML generation, so JSON generated without -html can have inReport: false even for instrumented methods.

An edge has caller, callee, kind, count, and callSites:

{
  "caller": "<demo.Main: void run()>",
  "callee": "<demo.Dog: void speak()>",
  "kind": "observed",
  "count": 1,
  "callSites": [
    {
      "id": "7",
      "declared": "<demo.Animal: void speak()>",
      "kind": "virtual"
    }
  ]
}

This is an illustrative individual edge, not a complete report. Edge kind is observed or declared. Each callsite's kind describes its invocation, such as virtual, interface, static, special, constructor, or dynamic; legacy records use legacy. A callsite ID is the original statement position within its caller, not a source line or a globally unique ID.

count sums distinct accepted records for the relationship. It is not the number of times the method was called. The same caller/callee pair can have separate observed and declared edges when different callsites have different confirmation status.

Statistic Meaning
observedEdges Aggregated edges with confirmed runtime targets
declaredEdges Aggregated fallback edges retaining declared targets
unresolvedSites Recorded callsites with no confirmed target
callSites Number of recorded callsites represented in the graph
records Number of distinct accepted call records

Edge counts and callsite counts differ because several callsites can share an edge, and one callsite can reach several targets. See Dynamic Call Graph for interpretation.

Read JSON in a script

This Python example requires only the standard library:

import json

with open("coverage.json", encoding="utf-8") as source:
    report = json.load(source)

for category, metric in report["coverageSummary"].items():
    print(category, metric["covered"], metric["total"], metric["percent"])

for edge in report.get("callGraph", {}).get("edges", []):
    if edge["kind"] == "observed":
        print(edge["caller"], "->", edge["callee"])

Runtime records

Coverage messages begin with CLASS=, METHOD=, STATEMENT=, ACTIVITY=, SERVICE=, BROADCASTRECEIVER=, or CONTENTPROVIDER=. Statement identities combine the method signature, original Jimple text, and position within the method.

The current call-tracing message has nine pipe-separated fields:

MC2|trace UUID|sequence|status|site|caller|declared|actual|invocation kind

D records a reached callsite and its declared target; its actual field is empty. O records a confirmed actual target. Signatures are URL-encoded with UTF-8 form encoding, so decode + as a space. A process trace UUID and sequence identify each record. The runtime deduplicates declarations and actual-target pairs per callsite within a process.

The parser validates the tag, payload, callsite, and signatures against the original APK baseline. Duplicate call-record identities are ignored. Malformed, truncated, or incompatible records are rejected, but the current report does not provide a rejection-count breakdown. Legacy CALL= records retain only declared targets.

Timestamps and combining captures

Use adb logcat -v threadtime for the expected MM-DD HH:mm:ss prefix. The aggregate parser accepts several tagged logcat formats, including year-prefixed timestamps. The current timeline extractor accepts only a leading MM-DD HH:mm minute. Untimestamped and year-prefixed observations can therefore contribute to aggregate totals without entering the timeline.

Distinct coverage probes are assigned to their first matching occurrence in file order. Keep logs chronological when building a timeline. The minute key has no year, so do not use it as an unambiguous multi-year time series.

For aggregate coverage, logs from the same APK, probes, scope, and tag can be concatenated and parsed with -pa. Coverage entities and repeated MC2 records are deduplicated. Different process UUIDs preserve distinct call observations across executions. This produces an aggregate union, not a run comparison or an invocation-frequency profile. Retain individual logs if you need to distinguish runs.

Clone this wiki locally