This repository has been archived by the owner on May 10, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 443
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
- Loading branch information
1 parent
9504684
commit 036165e
Showing
5 changed files
with
176 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
// Copyright 2024 The Brave Authors. All rights reserved. | ||
// This Source Code Form is subject to the terms of the Mozilla Public | ||
// License, v. 2.0. If a copy of the MPL was not distributed with this | ||
// file, You can obtain one at https://mozilla.org/MPL/2.0/. | ||
|
||
import Foundation | ||
import Preferences | ||
|
||
/// Monitors how long the browser is foregrounded to answer the `Brave.Uptime.BrowserOpenTime` P3A question | ||
public class UptimeMonitor { | ||
private var timer: Timer? | ||
|
||
private(set) static var usageInterval: TimeInterval = 15 | ||
private(set) static var now: () -> Date = { .now } | ||
private(set) static var calendar: Calendar = .current | ||
|
||
public init() { | ||
if Preferences.UptimeMonitor.startTime.value == nil { | ||
// If today is the first time monitoring uptime, set the frame start time to now. | ||
resetPrefs() | ||
} | ||
recordP3A() | ||
} | ||
|
||
deinit { | ||
timer?.invalidate() | ||
} | ||
|
||
// For testing | ||
var didRecordP3A: ((_ durationInMinutes: Int) -> Void)? | ||
|
||
public var isMonitoring: Bool { | ||
timer != nil | ||
} | ||
|
||
/// Begins a timer to monitor uptime | ||
public func beginMonitoring() { | ||
if isMonitoring { | ||
return | ||
} | ||
timer = Timer.scheduledTimer(withTimeInterval: Self.usageInterval, repeats: true, block: { [weak self] _ in | ||
guard let self else { return } | ||
Preferences.UptimeMonitor.uptimeSum.value += Self.usageInterval | ||
self.recordP3A() | ||
}) | ||
} | ||
/// Pauses the timer to monitor uptime | ||
public func pauseMonitoring() { | ||
timer?.invalidate() | ||
timer = nil | ||
} | ||
|
||
private func recordP3A() { | ||
guard let startTime = Preferences.UptimeMonitor.startTime.value, | ||
!Self.calendar.isDate(startTime, inSameDayAs: Self.now()) else { | ||
// Do not report, since 1 day has not passed. | ||
return | ||
} | ||
let buckets: [Bucket] = [ | ||
.r(0...30), | ||
.r(31...60), | ||
.r(61...120), // 1-2 hours | ||
.r(121...180), // 2-3 hours | ||
.r(181...300), // 3-5 hours | ||
.r(301...420), // 5-7 hours | ||
.r(421...600), // 7-10 hours | ||
.r(601...) // 10+ hours | ||
] | ||
let durationInMinutes = Int(Preferences.UptimeMonitor.uptimeSum.value / 60.0) | ||
UmaHistogramRecordValueToBucket("Brave.Uptime.BrowserOpenMinutes", buckets: buckets, value: durationInMinutes) | ||
resetPrefs() | ||
didRecordP3A?(durationInMinutes) | ||
} | ||
|
||
private func resetPrefs() { | ||
Preferences.UptimeMonitor.startTime.value = Self.now() | ||
Preferences.UptimeMonitor.uptimeSum.value = 0 | ||
} | ||
|
||
static func setUsageIntervalForTesting(_ usageInterval: TimeInterval) { | ||
Self.usageInterval = usageInterval | ||
} | ||
|
||
static func setNowForTesting(_ now: @escaping () -> Date) { | ||
Self.now = now | ||
} | ||
|
||
static func setCalendarForTesting(_ calendar: Calendar) { | ||
Self.calendar = calendar | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
// Copyright 2024 The Brave Authors. All rights reserved. | ||
// This Source Code Form is subject to the terms of the Mozilla Public | ||
// License, v. 2.0. If a copy of the MPL was not distributed with this | ||
// file, You can obtain one at https://mozilla.org/MPL/2.0/. | ||
|
||
import Foundation | ||
import XCTest | ||
import Preferences | ||
@testable import Growth | ||
|
||
class UptimeMonitorTests: XCTestCase { | ||
|
||
// How much a single second is actually accounted for during the test | ||
static let testSecond: TimeInterval = 0.01 | ||
|
||
override func setUp() { | ||
super.setUp() | ||
|
||
Preferences.UptimeMonitor.startTime.reset() | ||
Preferences.UptimeMonitor.uptimeSum.reset() | ||
|
||
var testCalendar = Calendar(identifier: .gregorian) | ||
testCalendar.timeZone = .init(abbreviation: "GMT")! | ||
testCalendar.locale = .init(identifier: "en_US_POSIX") | ||
UptimeMonitor.setCalendarForTesting(testCalendar) | ||
UptimeMonitor.setNowForTesting({ .now }) | ||
UptimeMonitor.setUsageIntervalForTesting(Self.testSecond) | ||
} | ||
|
||
func testNoStartTimeInit() { | ||
XCTAssertNil(Preferences.UptimeMonitor.startTime.value) | ||
let um = UptimeMonitor() | ||
XCTAssertNotNil(Preferences.UptimeMonitor.startTime.value) | ||
XCTAssertFalse(um.isMonitoring) | ||
} | ||
|
||
func testRecordAfterDay() { | ||
let um = UptimeMonitor() | ||
let e = expectation(description: "recorded") | ||
let now = Date() | ||
UptimeMonitor.setNowForTesting({ now }) | ||
Preferences.UptimeMonitor.startTime.value = .now.addingTimeInterval(-60*60*24) | ||
Preferences.UptimeMonitor.uptimeSum.value = 60 | ||
um.didRecordP3A = { minutes in | ||
XCTAssertEqual(minutes, 1) | ||
e.fulfill() | ||
} | ||
um.beginMonitoring() | ||
wait(for: [e], timeout: Self.testSecond * 2) | ||
um.pauseMonitoring() | ||
|
||
// Ensure prefs are reset | ||
XCTAssertEqual(Preferences.UptimeMonitor.startTime.value, now) | ||
XCTAssertEqual(Preferences.UptimeMonitor.uptimeSum.value, 0) | ||
} | ||
|
||
func testNoRecordBeforeOneDay() { | ||
let um = UptimeMonitor() | ||
let now = Date() | ||
UptimeMonitor.setNowForTesting({ now }) | ||
let e = expectation(description: "not-recorded") | ||
e.isInverted = true | ||
Preferences.UptimeMonitor.startTime.value = now | ||
Preferences.UptimeMonitor.uptimeSum.value = 60 | ||
um.didRecordP3A = { _ in | ||
XCTFail("Should not record any data before a day has passed") | ||
} | ||
um.beginMonitoring() | ||
wait(for: [e], timeout: Self.testSecond * 2) | ||
um.pauseMonitoring() | ||
|
||
// Ensure prefs are not reset | ||
XCTAssertEqual(Preferences.UptimeMonitor.startTime.value, now) | ||
XCTAssertNotEqual(Preferences.UptimeMonitor.uptimeSum.value, 0) | ||
} | ||
} |