{"id":19041047,"url":"https://github.com/everythingme/probe","last_synced_at":"2025-04-23T21:26:09.454Z","repository":{"id":145220150,"uuid":"48459862","full_name":"EverythingMe/probe","owner":"EverythingMe","description":"Android performance instrumentation tool","archived":false,"fork":false,"pushed_at":"2015-12-24T08:57:02.000Z","size":562,"stargazers_count":41,"open_issues_count":0,"forks_count":7,"subscribers_count":7,"default_branch":"master","last_synced_at":"2025-04-18T06:51:07.167Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/EverythingMe.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2015-12-23T00:04:59.000Z","updated_at":"2024-09-23T11:22:25.000Z","dependencies_parsed_at":"2023-04-16T20:46:24.734Z","dependency_job_id":null,"html_url":"https://github.com/EverythingMe/probe","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EverythingMe%2Fprobe","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EverythingMe%2Fprobe/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EverythingMe%2Fprobe/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EverythingMe%2Fprobe/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/EverythingMe","download_url":"https://codeload.github.com/EverythingMe/probe/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250516271,"owners_count":21443592,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-08T22:26:38.298Z","updated_at":"2025-04-23T21:26:09.435Z","avatar_url":"https://github.com/EverythingMe.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Probe\n\nAndroid applications performance testing tool.\n\n\n#Usage\nThere are two ways to use probe:\n\n1. As a command line tool. This will run a basic test case of Restart ----\u003e Steady state\n\n\t```sh\n\t\n\t# python console.py --package your.package.name --activity TheActivityToRunProbeOn --repeat-count x --timeout y\n\t\n\t# python console.py --help\n\t\n\tOptions:\n\t  --package TEXT          Package name to run Probe on\n\t  --activity TEXT         Activity to restart within that package\n\t  --apk-path TEXT         Optional: Path to installed APK, if you want APK analisys\n\t  --repeat-count INTEGER  Times to repeat the test\n\t  --timeout INTEGER       probe will stop when logcat output is silent for\n\t                          that duration\n\t  --device-id TEXT        device_id to send commands to\n\t```\n\n2. As a python module, as a part of your UI tests start probe at the begining of each test, and stop in when it ends (the following example shows how we did that with [Magneto](https://github.com/MagnetoTesting/magneto), setting up probe inside our `BaseTestCase`\n\t\n\t```python \n\tfrom probe.probe import Probe\n\t\n\t\n\tclass BaseTestCase(TestCase):\n\t\tprobe = None\n\t\t\t\n\t\tdef setup_method(self, method):\n\t       super(BaseTestCase, self).setup_method(method)\n\t       self.probe = Probe(package, activity, device_id)\n\t       probe.start()\n\t       \n\t   def teardown_method(self, method):\n\t   \t\tsuper(BaseTestCase, self).teardown_method(method)\n\t   \t\tself.probe.stop()\n   \n\t```\n\n##Background\nIn EverythingMe we used to have \"performance blitzs\" every now and then, when we (subjectivly) felt EverythingMe's launcher performance is not on par with what we would expect it to be. Then we would start manually measuring the app's vital signs using the following tools: \n\n* **Memory utilization** - Dalvik, Native, PSS total:\n\n\t```bash\n\tadb shell dumpsys meminfo app.package.name\n\t```\n* **CPU utilization** - this one is tricky, you can certainly measure it with `top`, but in order to calculate \"how much processing power was needed to complete the task you'd need to sum up the instantaneous CPU usage for the entire duration of the measurement. Instead, I chose to measure CPU ticks using data from proc stat\n\n\t```bash\n\tcat proc/{pid}/stat\n\t```\n\t\n* **Disk usage** - well, a simple `du` covers it\n\n\t```bash\n\tdu -s /data/data/{package_name}\n\t```\n* **Thread count** - using `top` (#THR column) \n* **a lot more** - with Android Studio's new(ish) tools, MAT and numerous logcat logs.\n\n\nYou are probably familiar with the huge amount of repetative work when doing such measurements and even though this is a tedious process, the collected results worth nothing without a comparison to previous measurements. This is where `probe` gets in the picture.\n\nprobe is desigend to measure these vital signs mentioned above (and much more), as a cli tool or as a python module.\n\n##How does it work\n \nProbe is designed to measure everything* automatically, and by everything I mean anything that has a logcat output or which can be queried using adb (dumpsys, dumpheap, profile, etc.) or a linux command (du, cat, top, etc.)\n\nEach measurer registers to a certain registar, and the latter in turn triggers that measurer to do something.\n\nThere are three types of registrars:\n\n##Registrars\n\nRegistars are metaclasses. when a class's metaclass is being set to be a registrar, it is automatically being registered to start/stop events and logcat output events.\n\n**SnapshotRegistrar**:  *more here soon*\n\n**ContinuousRegistrar**: *more here soon*\n\n**GlobalRegistrar**: *more here soon*\n \n##Measurers\n\nThe actual code which collects the data is written here.\n\n**Snapshot measurers**: When probe is triggered to stop measuring, it begins collecting data from all measurers registered to `SnapshotRegistrar`. It may get an instantaneous value of a measurer (current memory utilization, number of threads, CPU ticks, etc.)\n\n**Continuous measurers**:  When probe starts, it triggers an `adb logcat` instance. This instance output is being passed line by line to all measurers which registered to the `ContinuousRegistrar`. Each measurer filters relevant logcat lines using `is_matching(line)` function, and processes then in `process(line)`. The measurer handles its own state machine (whether its a simple line counter which only measures the amount of processed lines, or a more complicated one measuring the frequency of `GC_FOR_ALLOC` lines).\n\n**Global measurers**: A combination of both, a Continuous measurer, receiving relevant logcat lines, processing them (either just counting occurrences of this logcat line, or triggering a small state machine in it). the registar then takes a snapshot of what it accumulated when probe triggers it to stop.\n \n \n \n* **Writing new measurers**: \n\n This is how a measurer looks like:\n \n\t```python       \n\tclass CpuTicksUser(object):\n\t   \"\"\"\n\t   Cpu usage - user space (in ticks)\n\t   \"\"\"\n\t   PROC_STAT_USER_TICKS_LOC = 13\n\t   \n\t   __metaclass__ = SnapshotRegistrar\n\t\n\t   def __init__(self):\n\t       pass\n\t\n\t   def name(self):\n\t\t   \"\"\"\n\t\t   the key of this measurement in the output dict \n\t\t   \"\"\"\n\t       return 'cpu_ticks_user'\n\t\n\t   def value(self):\n\t\t   \"\"\"\n\t\t   the value of this measurement in the output dict \n\t\t   \"\"\"\n\t   \t\tpid = runtime.get_instance().get_pid()\n\t   \t\tprocstat = adb.get_instance().shell('cat /proc/%s/stat' % pid)['stdout'][0].split()\n\t   \t\treturn int(procstat[PROC_STAT_USER_TICKS_LOC])    \n\t```\n\n\n## Querying probe's DB \nProbe writes output to a provided DB (or a local sql lite (mydatabase.db) if no connection string is provided.\n\nquerying probe's db for `cpu_ticks` (both user and kernel ticks) values in last 100 builds:\n\n```sql\nSELECT p.Build_number, version_code, Cpu_ticks_user, Cpu_ticks_kernel FROM collector p\nJOIN ((SELECT Build_number, max(Created_at) as max_time\nFROM collector\nGROUP BY Build_number)) m ON (p.Build_number=m.Build_number and p.created_at=m.max_time)\nORDER BY p.Build_number desc\nLIMIT 100\n```\n\n\nAnd that's how it looks like in [redash](https://github.com/getredash/redash): \n![cpu utilization](probe-cpu-utilization.png)\n\n## Important note\n**It is important to emphesize that probe is a benchmark to itself! meaning it needs to run consecutively on the same device in order to produce valid results (of course SGS4 would have a higher PSS total value than SGS3, it has 225% the pixle count of SGS3).**\n\n##Some more graphs \nor \"this is what we measured on EverythingMe Launcher\"\n![some more graphs](graphs.png)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feverythingme%2Fprobe","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Feverythingme%2Fprobe","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feverythingme%2Fprobe/lists"}