MetriKit is a new framework for collecting and processing batteries and performance metrics in iOS 13. This is WWDC working with XCTestMetrics and Xcode Metrics organizers this year as part of a concerted effort to bring new insights to developers about the performance of their applications in this area.
Apple will automatically collect Metrics from apps installed on the app store. You can select a new Metrics tab in Xcode 11 by opening the organizer (⌥⌘⇧o) and selecting it.
MetriKit complements Xcode organizer metrics by providing a programmatic way to receive day-to-day information about the performance of applications in this area. With this information, you can collect, aggregate, and analyze more detailed information yourself than through Xcode.
Gift bag at the end of the article
Understand application metrics
Metrics can help you discover problems that you might not see when testing locally, and allow you to track and change different versions of your application. In this initial release, apple focused on two of the most important metrics for users: battery usage and performance.
Battery usage
Battery life depends on many different factors. Physical aspects, such as the age of the device and the number of charging cycles, are decisive.
But the way you use your phone is also important.
Things like the use of CPU, the brightness of the monitor and the color on the screen, and the frequency that the radio uses to get data or get the current location - all of which have a big impact. But the most important thing to remember is that users are very concerned about the battery life.
Apart from how good the camera is, the time between charges is also very long. This is the deciding factor when someone buys a new cell phone these days.
So when their new, expensive cell phones don't go through the day, they will be very unhappy.
Until recently, apple took most of the blame for the battery problem.
But since iOS 12 and its new battery usage screen are set up, users can now judge when their favorite app is being blamed.
Fortunately, with iOS 13, you now have everything you need to make sure your app doesn't conflict with reasonable energy use.
performance
Performance is another key factor in the overall user experience. Under normal circumstances, we may look at data, such as processor clock speed or frame rate, as a measure of performance. Instead, apple focuses on less abstract and more operational metrics:
Hanging rate
How often is the main / UI thread blocked so that the application does not respond to user input?
Launch time
How long does it take for the application to work after the user clicks the icon?
Peak memory & suspended memory
How much memory does the application use at peak before entering the background?
Disk write
How often does the app write to disk, and if you don't know yet, it's relatively slow (even the flash on the iPhone! )
Using MetriKit
From the perspective of API users, it's hard to imagine how 'MetriKit' is easier to merge. All you need is some part of your application as a measurement subscriber (an obvious choice is your 'AppDelegate') and add it to the shared 'MXMetricManager:`
import UIKit import MetricKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { MXMetricManager.shared.add(self) return true } func applicationWillTerminate(_ application: UIApplication) { MXMetricManager.shared.remove(self) } } extension AppDelegate: MXMetricManagerSubscriber { func didReceive(_ payloads: [MXMetricPayload]) { ... } }
iOS automatically collects samples when using the application and sends aggregate reports containing these metrics every day (every 24 hours).
To verify that your 'MXMetricManagerSubscriber' is calling its delegate method as expected, select the SIMPLE MetriKit payload from the Debug menu when Xcode runs the application.
The simulate MetriKit payload menu item requires the application to run on the actual device and be disabled for simulator build.
Marking key code sections with road signs
In addition to the baseline statistics collected for you, you can use the 'mxSignpost' function to collect the most important part of your code. This roadmap supports the API to capture CPU time, memory, and write to disk.
For example, if part of the application does post process the audio stream, you can annotate these areas with metric road signs to determine the energy and performance impact of the work:
let audioLogHandle = MXMetricManager.makeLogHandle(category: "Audio") func processAudioStream() { mxSignpost(.begin, log: audioLogHandle, name: "ProcessAudioStream") ... mxSignpost(.end, log: audioLogHandle, name: "ProcessAudioStream") }
Create a self hosted Web service to collect AppMetrics
Now that you have the information, what will you do? How can we fill in the "..." placeholder in the implementation "didreceive"?`
You, you can transfer it to some paid analysis or accident reporting service, but what's the fun of that? Let's build our own Web services to collect these for further analysis:
Using PostgreSQL to store and query metrics
The MXMetricPayload object received by the measurement manager subscriber has a convenient jsonRepresentation() method to generate the following:
[Click to view the JSON representation ]
As you can see, there are many places for this representation. Defining patterns for all this information will be a daunting task, and there is no guarantee that this will not change in the future. So let's take the NoSQL paradigm (though responsibly using postgress) by storing payloads in the JSONB column:
CREATE TABLE IF NOT EXISTS metrics (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
payload JSONB NOT NULL
);
We can use JSON operators like this:
SELECT (payload -> 'applicationTimeMetrics' ->> 'cumulativeForegroundTime')::INTERVAL FROM metrics; -- interval -- ═══════════════════ -- @ 11 mins 40 secs -- (1 row)
The JSON representation of the measure stores the time and memory measures as strings with units (such as "100 ms" and 500 kB). In Postgres, you can convert a time measure directly to the INTERVAL type, but you need to create a function that converts to a byte count:
CREATE OR REPLACE FUNCTION parse_byte_count (TEXT) RETURNS BIGINT AS $$ SELECT replace(split_part($1, ' ', 1),',','')::BIGINT * CASE split_part($1, ' ', 2) WHEN 'kB' THEN 1024 WHEN 'MB' THEN 1024 * 1024 WHEN 'GB' THEN 1024 * 1024 * 1024 END $$ LANGUAGE 'sql' STRICT IMMUTABLE;
Advanced: creating views
JSON operators in PostgreSQL can be cumbersome to use -- especially for more complex queries. One way to help is to create a view. (materialize or otherwise) project the most important information to you in the most convenient way:
DROP VIEW key_performance_indicators; CREATE VIEW key_performance_indicators AS SELECT id, (payload -> 'appVersion') AS app_version, (payload -> 'metaData' ->> 'deviceType') AS device_type, (payload -> 'metaData' ->> 'regionFormat') AS region, (payload -> 'applicationTimeMetrics' ->> 'cumulativeForegroundTime' )::INTERVAL AS cumulative_foreground_time, parse_byte_count( payload -> 'memoryMetrics' ->> 'peakMemoryUsage' ) AS peak_memory_usage_bytes FROM metrics;
Using views, you can perform aggregate queries. In all your metrics, JSON payloads have the convenience of relational databases supported by patterns:
SELECT avg(cumulative_foreground_time) FROM key_performance_indicators; -- avg -- ══════════════════ -- @ 9 mins 41 secs SELECT app_version, percentile_disc(0.5) WITHIN GROUP (ORDER BY peak_memory_usage_bytes) AS median FROM key_performance_indicators GROUP BY app_version; -- app_version │ median -- ═════════════╪═══════════ -- "1.0.1" │ 192500000 -- "1.0.0" │ 204800000
PostgreSQL doesn't handle CamelCase's table or column names very well, so keep this in mind when using the following functions.
Create Web Service
In this case, most of the heavy work is delegated to Postgres, which makes the server-side implementation quite boring. For completeness, here are some reference implementations in Ruby(Sinatra) and JavaScript(Express):
require 'sinatra/base' require 'pg' require 'sequel' class App < Sinatra::Base configure do DB = Sequel.connect(ENV['DATABASE_URL']) end post '/collect' do DB[:metrics].insert(payload: request.body.read) status 204 end end
Send metrics as JSON
Now that we have set everything up, the last step is to implement the required 'MXMetricManagerSubscriber' delegation method 'didReceive(?)' to pass these information to our web service:
extension AppDelegate: MXMetricManagerSubscriber { func didReceive(_ payloads: [MXMetricPayload]) { for payload in payloads { let url = URL(string: "https://example.com/collect")! var request = URLRequest(url: url) request.httpMethod = "POST" request.httpBody = payload.jsonRepresentation() let task = URLSession.shared.dataTask(with: request) task.priority = URLSessionTask.lowPriority task.resume() } } }
1024 gift package
Join the iOS development exchange QQ group:[ 1012951431 ], choose to join in communication, study together and share learning materials. Looking forward to your joining! (you can get gift bag when you enter the group)
Reprint address: https://nshipster.com/metrickit/