169 lines
5.2 KiB
Go
169 lines
5.2 KiB
Go
package main
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
// Relationship extraction for the Logcat visual map — ported to Go so the mining
|
|
// heuristics live in native (compiled) code rather than shipped JavaScript.
|
|
//
|
|
// A log line is just text, but Android's framework + system-event logs encode
|
|
// real relationships: who started whom, who crashed, who got killed, who sent a
|
|
// signal to which pid. We mine those so the map can draw meaningful edges on top
|
|
// of the ambient co-occurrence web. Runs in the existing per-line log pipeline
|
|
// (parseLogcatLine), so the result ships attached to each LogcatLine — no extra IPC.
|
|
|
|
// LogRef is one relationship a log line implies. JSON shape matches the frontend.
|
|
type LogRef struct {
|
|
Kind string `json:"kind"` // activity|spawn|death|crash|anr|signal|gfx|mention
|
|
Target string `json:"target"` // package, component, or pid payload
|
|
TargetKind string `json:"targetKind"` // package|component|pid
|
|
}
|
|
|
|
var (
|
|
lcpPkg = regexp.MustCompile(`\b([a-z][a-z0-9_]*(?:\.[a-z0-9_]+){2,})\b`)
|
|
lcpPkgAnchor = regexp.MustCompile(`^[a-z][a-z0-9_]*(?:\.[a-z0-9_]+){2,}$`)
|
|
lcpComponent = regexp.MustCompile(`(?i)([a-z][a-z0-9_.]+)/([a-z0-9_.$]+)`)
|
|
lcpEventCSV = regexp.MustCompile(`\[([^\]]*)\]`)
|
|
lcpSig = regexp.MustCompile(`Sending signal\.\s*PID:\s*(\d+)`)
|
|
lcpProcess = regexp.MustCompile(`(?i)Process:\s*([a-z][a-z0-9_.]+)`)
|
|
lcpANRin = regexp.MustCompile(`\bANR in\b`)
|
|
lcpStartProc = regexp.MustCompile(`\bStart proc\b`)
|
|
lcpKilling = regexp.MustCompile(`\bKilling\b|\bhas died\b|\bdied\b`)
|
|
lcpStartAct = regexp.MustCompile(`\bSTART u\d+|\bDisplayed\b|\bmoveTaskTo`)
|
|
lcpFatal = regexp.MustCompile(`FATAL EXCEPTION`)
|
|
lcpGfxTag = regexp.MustCompile(`^(SurfaceFlinger|WindowManager|ViewRootImpl|Choreographer|gralloc|OpenGLRenderer)`)
|
|
lcpMentSkip = regexp.MustCompile(`^(java|javax|sun|kotlin|android|androidx|dalvik)\.`)
|
|
)
|
|
|
|
func lcpFirstPackage(s string) string {
|
|
if m := lcpPkg.FindStringSubmatch(s); m != nil {
|
|
return m[1]
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// Pull the package field out of an event-log CSV payload (first dotted token).
|
|
func lcpEventPackage(msg string) string {
|
|
csv := lcpEventCSV.FindStringSubmatch(msg)
|
|
if csv == nil {
|
|
return lcpFirstPackage(msg)
|
|
}
|
|
for _, f := range strings.Split(csv[1], ",") {
|
|
t := strings.TrimSpace(f)
|
|
if lcpPkgAnchor.MatchString(t) {
|
|
return t
|
|
}
|
|
}
|
|
return lcpFirstPackage(msg)
|
|
}
|
|
|
|
// lcpExtractRefs mines the relationships a single log line implies. Returns an
|
|
// empty slice for the vast majority of lines.
|
|
func lcpExtractRefs(tag, msg string) []LogRef {
|
|
refs := []LogRef{}
|
|
push := func(kind, target, targetKind string) {
|
|
if target != "" {
|
|
refs = append(refs, LogRef{Kind: kind, Target: target, TargetKind: targetKind})
|
|
}
|
|
}
|
|
|
|
// binary event-log tags (events buffer)
|
|
switch tag {
|
|
case "am_proc_start", "am_proc_bound":
|
|
push("spawn", lcpEventPackage(msg), "package")
|
|
return refs
|
|
case "am_proc_died", "am_kill", "am_low_memory":
|
|
push("death", lcpEventPackage(msg), "package")
|
|
return refs
|
|
case "am_crash":
|
|
push("crash", lcpEventPackage(msg), "package")
|
|
return refs
|
|
case "am_anr":
|
|
push("anr", lcpEventPackage(msg), "package")
|
|
return refs
|
|
case "am_activity_launch_time", "am_focused_activity", "am_resume_activity", "am_pause_activity", "wm_focused_window":
|
|
if c := lcpComponent.FindStringSubmatch(msg); c != nil {
|
|
push("activity", c[1]+"/"+c[2], "component")
|
|
} else {
|
|
push("activity", lcpEventPackage(msg), "package")
|
|
}
|
|
return refs
|
|
}
|
|
|
|
// framework text logs (main/system buffers)
|
|
if tag == "ActivityManager" || tag == "ActivityTaskManager" {
|
|
if lcpANRin.MatchString(msg) {
|
|
push("anr", lcpFirstPackage(msg), "package")
|
|
}
|
|
if lcpStartProc.MatchString(msg) {
|
|
push("spawn", lcpFirstPackage(msg), "package")
|
|
}
|
|
if lcpKilling.MatchString(msg) {
|
|
push("death", lcpFirstPackage(msg), "package")
|
|
}
|
|
if sig := lcpSig.FindStringSubmatch(msg); sig != nil {
|
|
push("signal", sig[1], "pid")
|
|
}
|
|
if lcpStartAct.MatchString(msg) {
|
|
if c := lcpComponent.FindStringSubmatch(msg); c != nil {
|
|
push("activity", c[1]+"/"+c[2], "component")
|
|
} else {
|
|
push("activity", lcpFirstPackage(msg), "package")
|
|
}
|
|
}
|
|
if len(refs) > 0 {
|
|
return refs
|
|
}
|
|
}
|
|
|
|
if tag == "AndroidRuntime" || lcpFatal.MatchString(msg) {
|
|
if p := lcpProcess.FindStringSubmatch(msg); p != nil {
|
|
push("crash", p[1], "package")
|
|
} else {
|
|
push("crash", lcpFirstPackage(msg), "package")
|
|
}
|
|
if len(refs) > 0 {
|
|
return refs
|
|
}
|
|
}
|
|
|
|
if tag == "lowmemorykiller" || tag == "lmkd" {
|
|
push("death", lcpFirstPackage(msg), "package")
|
|
if len(refs) > 0 {
|
|
return refs
|
|
}
|
|
}
|
|
|
|
if lcpGfxTag.MatchString(tag) {
|
|
if c := lcpComponent.FindStringSubmatch(msg); c != nil {
|
|
push("gfx", c[1]+"/"+c[2], "component")
|
|
return refs
|
|
}
|
|
}
|
|
|
|
return refs
|
|
}
|
|
|
|
// lcpExtractMentions: generic fallback — up to `max` package-looking tokens
|
|
// (used when the map's "parsed mentions" toggle is on).
|
|
func lcpExtractMentions(msg string, max int) []LogRef {
|
|
out := []LogRef{}
|
|
seen := map[string]bool{}
|
|
for _, m := range lcpPkg.FindAllStringSubmatch(msg, -1) {
|
|
if len(out) >= max {
|
|
break
|
|
}
|
|
t := m[1]
|
|
if seen[t] {
|
|
continue
|
|
}
|
|
seen[t] = true
|
|
if lcpMentSkip.MatchString(t) {
|
|
continue
|
|
}
|
|
out = append(out, LogRef{Kind: "mention", Target: t, TargetKind: "package"})
|
|
}
|
|
return out
|
|
}
|