Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Try to reduce time taken to get a trackable online #481

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,7 @@ jobs:
- name: Build LogParserExample app
run: swift build
working-directory: Tools/Library/LogParserExample

- name: Build AnalyzeLocationEventFrequencies app
run: swift build
working-directory: Tools/Analysis/AnalyzeLocationEventFrequencies
8 changes: 7 additions & 1 deletion Sources/AblyAssetTrackingPublisher/DefaultPublisher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -263,12 +263,18 @@ extension DefaultPublisher {
ablyPublisher.subscribeForChannelStateChange(trackable: event.trackable)

trackables.insert(event.trackable)
//Start updating location only after the first trackable
if (trackables.count == 1){
//Start updating location only after the first trackable
locationService.startRecordingLocation()
locationService.startUpdatingLocation()
} else {
// We do this to increase the chances of receiving an enhanced location update for this trackable, so that the trackable can move into the online connection state as quickly as possible (see the hasSentAtLeastOneLocation check inside handleConnectionStateChange).

// If we did not do this, then (since startUpdatingLocation has already been called on the location service) in the case of a slowly-moving or stationary device, the trackable would not move into the online connection state until the device had moved a distance deemed worthy (by the resolution policy) of a new location update.
locationService.requestLocationUpdate()
}
resolveResolution(trackable: event.trackable)

hooks.trackables?.onTrackableAdded(trackable: event.trackable)
event.completion.handleSuccess()
duplicateTrackableGuard.finishAddingTrackableWithId(event.trackable.id, result: .success)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,17 @@ class DefaultLocationService: LocationService {
locationManager.systemLocationManager.stopUpdatingLocation()
}

func requestLocationUpdate() {
/*
From reading the ``CLLocationManager/startUpdatingLocation`` documentation, this seems to be the only programmatic way to provoke it into emitting a location event:

> Calling this method several times in succession does not automatically result in new events being generated. Calling stopUpdatingLocation() in between, however, does cause a new initial event to be sent the next time you call this method.
*/
logHandler?.debug(message: "Received requestLocationUpdate", error: nil)
locationManager.systemLocationManager.stopUpdatingLocation()
locationManager.systemLocationManager.startUpdatingLocation()
Comment on lines +82 to +83
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder how this affects the Mapbox SDK 🤔

  1. Have you checked whether this doesn't break things like location history recording?
  2. Will there be a big delay in location updates between the "stop" and "start" operations?
  3. Additionally, do we know what happens when you stop and start location updates? Will it consume a lot of resources to do that?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are all good questions, I'll look into it and get back to you 🙂

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a script for analysing the logs from the publisher example app, to find out if the frequencies of location updates and recorded locations is affected. My brief investigation using the iOS simulator suggests that they are not. However, I want to test on a device too, and for that we'll probably need to add the ability to log to a file. Once I've done that and gathered some results from a device, I'll post my findings here in detail.

As for resources (which I'm mainly taking to mean battery life), I do not know what the impact would be. I could have a think about how to investigate it, but I don't have any ideas off the top of my head.

}

enum LocationHistoryTemporaryStorageConfiguration {
private static let fileManager = FileManager.default

Expand Down Expand Up @@ -165,6 +176,7 @@ extension DefaultLocationService: PassiveLocationManagerDelegate {
}

func passiveLocationManager(_ manager: PassiveLocationManager, didUpdateLocation location: CLLocation, rawLocation: CLLocation) {
logHandler?.debug(message: "passiveLocationManager.didUpdateLocation", error: nil)
delegate?.locationService(sender: self, didUpdateRawLocationUpdate: RawLocationUpdate(location: rawLocation.toLocation()))
delegate?.locationService(sender: self, didUpdateEnhancedLocationUpdate: EnhancedLocationUpdate(location: location.toLocation()))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,6 @@ protocol LocationService: AnyObject {
func startRecordingLocation()
func stopRecordingLocation(completion: @escaping ResultHandler<LocationRecordingResult?>)
func changeLocationEngineResolution(resolution: Resolution)
/// Requests that the location service emit a new location update as soon as possible. The location service will make a best effort to ensure that, shortly after this method is called, its delegate receives a ``LocationServiceDelegate/locationService(sender:didUpdateRawLocationUpdate)`` and ``LocationServiceDelegate/locationService(sender:didUpdateEnhancedLocationUpdate)`` event.
func requestLocationUpdate()
}
33 changes: 33 additions & 0 deletions Tests/PublisherTests/DefaultPublisher/DefaultPublisherTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,39 @@ class DefaultPublisherTests: XCTestCase {
XCTAssertEqual(publisher.activeTrackable, trackable)
}

func testAddSecondTrackable_callsRequestLocationUpdateOnLocationService() {
ablyPublisher.connectCompletionHandler = { completion in completion?(.success) }
let expectation = XCTestExpectation()
expectation.expectedFulfillmentCount = 2

// When tracking a trackable
publisher.add(trackable: trackable) { result in
switch result {
case .success:
expectation.fulfill()
case .failure:
XCTFail("Failure callback shouldn't be called")
}
}

// It should not yet have called requestLocationUpdate on the location service
XCTAssertFalse(locationService.requestLocationUpdateCalled)

// And then adding another trackable
publisher.add(trackable: Trackable(id: "TestAddedTrackableId1")) { result in
switch result {
case .success:
expectation.fulfill()
case .failure:
XCTFail("Failure callback shouldn't be called")
}
}
wait(for: [expectation], timeout: 5.0)

// It should call requestLocationUpdate on the location service
XCTAssertTrue(locationService.requestLocationUpdateCalled)
}

func testAdd_track_error() {
let errorInformation = ErrorInformation(type: .publisherError(errorMessage: "Test AblyPublisherService error"))
ablyPublisher.connectCompletionHandler = { completion in completion?(.failure(errorInformation)) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,9 @@ public class MockLocationService: LocationService {
stopRecordingLocationParamCompletion = completion
stopRecordingLocationCallback?(completion)
}

public var requestLocationUpdateCalled = false
public func requestLocationUpdate() {
requestLocationUpdateCalled = true
}
}
14 changes: 14 additions & 0 deletions Tools/Analysis/AnalyzeLocationEventFrequencies/Package.resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"pins" : [
{
"identity" : "swift-argument-parser",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser",
"state" : {
"revision" : "fddd1c00396eed152c45a46bea9f47b98e59301d",
"version" : "1.2.0"
}
}
],
"version" : 2
}
24 changes: 24 additions & 0 deletions Tools/Analysis/AnalyzeLocationEventFrequencies/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// swift-tools-version: 5.7
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "AnalyzeLocationEventFrequencies",
platforms: [
.macOS(.v12)
],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.2.0"),
.package(path: "../../Library/LogParser")
],
targets: [
.executableTarget(
name: "AnalyzeLocationEventFrequencies",
dependencies: [
.product(name: "LogParser", package: "LogParser"),
.product(name: "ArgumentParser", package: "swift-argument-parser")
]
),
]
)
57 changes: 57 additions & 0 deletions Tools/Analysis/AnalyzeLocationEventFrequencies/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# AnalyzeLocationEventFrequencies

This script is intended to help analyse the impact of restarting `CLLocationManager` each time `AblyAssetTrackingPublisher.DefaultLocationManager`’s `requestLocationUpdate` is called, specifically whether it negatively impacts:

- the frequency at which the SDK receives location updates
- the Mapbox SDK’s location recording

You can run it on a file containing log output from the publisher example app by running the following command from the current directory:

```bash
swift run AnalyzeLocationEventFrequencies <file>
```

There is an example log file `example.txt` that you can try this out on. It produces a CSV with contents like this:

|Timestamp (ISO 8601) |Event type |Time since last location update|Time since last recorded location|
|------------------------|-----------------------|-------------------------------|---------------------------------|
|2022-12-15T19:52:19.421Z|Recorded location | | |
|2022-12-15T19:52:20.035Z|Location update | | |
|2022-12-15T19:52:32.429Z|Recorded location | |13.007 |
|2022-12-15T19:52:32.443Z|Location update |12.408 | |
|2022-12-15T19:52:38.433Z|Recorded location | |6.004 |
|2022-12-15T19:52:38.441Z|Location update |5.998 | |
|2022-12-15T19:52:43.423Z|Recorded location | |4.990 |
|2022-12-15T19:52:43.439Z|Location update |4.998 | |
|2022-12-15T19:52:47.419Z|Recorded location | |3.996 |
|2022-12-15T19:52:47.434Z|Location update |3.995 | |
|2022-12-15T19:52:51.421Z|Recorded location | |4.002 |
|2022-12-15T19:52:51.442Z|Location update |4.008 | |
|2022-12-15T19:52:53.428Z|Recorded location | |2.007 |
|2022-12-15T19:52:53.603Z|Request location update| | |
|2022-12-15T19:52:53.615Z|Location update |2.173 | |
|2022-12-15T19:52:57.420Z|Recorded location | |3.992 |
|2022-12-15T19:52:57.429Z|Location update |3.814 | |
|2022-12-15T19:53:01.427Z|Recorded location | |4.007 |
|2022-12-15T19:53:01.433Z|Location update |4.004 | |
|2022-12-15T19:53:04.429Z|Recorded location | |3.002 |
|2022-12-15T19:53:04.445Z|Location update |3.012 | |
|2022-12-15T19:53:07.434Z|Recorded location | |3.005 |
|2022-12-15T19:53:07.447Z|Location update |3.002 | |
|2022-12-15T19:53:10.433Z|Recorded location | |2.999 |
|2022-12-15T19:53:10.443Z|Location update |2.996 | |
|2022-12-15T19:53:11.424Z|Recorded location | |0.991 |
|2022-12-15T19:53:11.923Z|Request location update| | |
|2022-12-15T19:53:11.928Z|Location update |1.485 | |
|2022-12-15T19:53:15.418Z|Recorded location | |3.994 |
|2022-12-15T19:53:15.433Z|Location update |3.505 | |
|2022-12-15T19:53:19.424Z|Recorded location | |4.006 |
|2022-12-15T19:53:19.439Z|Location update |4.006 | |
|2022-12-15T19:53:23.433Z|Recorded location | |4.010 |
|2022-12-15T19:53:23.448Z|Location update |4.009 | |
|2022-12-15T19:53:26.420Z|Recorded location | |2.987 |
|2022-12-15T19:53:26.436Z|Location update |2.988 | |
|2022-12-15T19:53:29.429Z|Recorded location | |3.009 |
|2022-12-15T19:53:29.445Z|Location update |3.009 | |
|2022-12-15T19:53:33.429Z|Recorded location | |4.000 |
|2022-12-15T19:53:33.443Z|Location update |3.998 | |
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
protocol CSVRowConvertible {
var csvRows: [String] { get }
}

protocol CSVRowWithColumnNamesConvertible: CSVRowConvertible {
static var csvHeaders: [String] { get }
}

enum CSVExport {
// A very rudimentary CSV export that does no quoting or escaping or anything like that.
static func export<T: CSVRowWithColumnNamesConvertible>(rows: [T]) -> String {
let csvRowStrings = ([T.csvHeaders] + rows.map(\.csvRows)).map { $0.joined(separator: ",") }
return csvRowStrings.joined(separator: "\n")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import Foundation

struct Event: Comparable, CSVRowWithColumnNamesConvertible {
var timestamp: Date
var type: EventType

enum EventType: CSVRowWithColumnNamesConvertible, Hashable {
case locationUpdate
case requestLocationUpdate
case recordedLocation

static var csvHeaders: [String] {
return ["Event type"]
}

var csvRows: [String] {
switch self {
case .locationUpdate:
return ["Location update"]
case .requestLocationUpdate:
return ["Request location update"]
case .recordedLocation:
return ["Recorded location"]
}
}
}

static func < (lhs: Event, rhs: Event) -> Bool {
return lhs.timestamp < rhs.timestamp
}

static func fromLogLine(_ logLine: LogLine) -> [Self] {
switch logLine {
case let .locationUpdate(timestamp):
return [.init(timestamp: timestamp, type: .locationUpdate)]
case let .requestLocationUpdate(timestamp):
return [.init(timestamp: timestamp, type: .requestLocationUpdate)]
case let .locationHistoryData(locationHistoryData):
return locationHistoryData.events.map { event in
return .init(timestamp: Date(timeIntervalSince1970: event.properties.time), type: .recordedLocation)
}
}
}

static var csvHeaders: [String] {
return ["Timestamp (ISO 8601)"] + EventType.csvHeaders
}

var csvRows: [String] {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withFractionalSeconds, .withInternetDateTime]
return [formatter.string(from: timestamp)] + type.csvRows
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import Foundation

struct EventWithCalculations: CSVRowWithColumnNamesConvertible {
var event: Event
var timeSinceLastOfType: TimeInterval?

static func fromEvents(_ events: [Event]) -> [EventWithCalculations] {
var lastTimestamps: [Event.EventType : Date] = [:]

return events.map { event in
let timeSinceLastOfType: TimeInterval?
if let lastTimestamp = lastTimestamps[event.type] {
timeSinceLastOfType = event.timestamp.timeIntervalSince(lastTimestamp)
} else {
timeSinceLastOfType = nil
}

lastTimestamps[event.type] = event.timestamp

return .init(event: event, timeSinceLastOfType: timeSinceLastOfType)
}
}

static var csvHeaders: [String] {
return Event.csvHeaders + ["Time since last location update", "Time since last recorded location"]
}

var csvRows: [String] {
let formattedTimeSinceLastOfType: String
if let timeSinceLastOfType {
formattedTimeSinceLastOfType = String(format: "%.3f", timeSinceLastOfType)
} else {
formattedTimeSinceLastOfType = ""
}

return event.csvRows + [
event.type == .locationUpdate ? formattedTimeSinceLastOfType : "",
event.type == .recordedLocation ? formattedTimeSinceLastOfType : ""
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/// A subset of AblyAssetTrackingCore’s same type.
struct LocationHistoryData: Codable {
var events: [GeoJSONMessage]

struct GeoJSONMessage: Codable {
var properties: GeoJSONProperties

struct GeoJSONProperties: Codable {
/**
Timestamp from a moment when measurment was done (in seconds since 1st of January 1970)
*/
let time: Double
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import Foundation
import LogParser

enum LogLine {
case locationUpdate(timestamp: Date)
case requestLocationUpdate(timestamp: Date)
case locationHistoryData(LocationHistoryData)

private struct KnownLogMessage {
var lastSubsystem: String
var prefix: String

static let locationUpdate = Self(lastSubsystem: "DefaultLocationService", prefix: "passiveLocationManager.didUpdateLocation")
static let requestLocationUpdate = Self(lastSubsystem: "DefaultLocationService", prefix: "Received requestLocationUpdate")

func matches(_ exampleAppSDKLine: ExampleAppSDKLogLine) -> Bool {
return exampleAppSDKLine.message.subsystems.last == lastSubsystem && exampleAppSDKLine.message.message.hasPrefix(prefix)
}
}

init?(exampleAppLine: ExampleAppLogFile.Line) {
switch exampleAppLine {
case let .other(line):
let prefix = "Received location history data: "
guard let prefixRange = line.range(of: prefix) else {
return nil
}

let jsonString = line[prefixRange.upperBound...]
let jsonData = jsonString.data(using: .utf8)!
let locationHistoryData = try! JSONDecoder().decode(LocationHistoryData.self, from: jsonData)
self = .locationHistoryData(locationHistoryData)
case let .sdk(exampleAppSDKLine):
if KnownLogMessage.locationUpdate.matches(exampleAppSDKLine) {
self = .locationUpdate(timestamp: exampleAppSDKLine.timestamp)
} else if KnownLogMessage.requestLocationUpdate.matches(exampleAppSDKLine) {
self = .requestLocationUpdate(timestamp: exampleAppSDKLine.timestamp)
} else {
return nil
}
}
}
}
Loading