Improves performance of launch jobs

Randall hit an issue where his device would take a few seconds to start
up. His logs indicate that his device is spending time running
LaunchJobs.

The first change here is to make sure we explicitly set the launch job
work at an ultra-high priority. This is okay, since we don't actually
present UI until after we finish these jobs. Running this work at
UserInteractive is appropriate since it quite literally prevents the
user from interacting with the app. There's also no other time-sensitive
UI work we need to be doing (like running animations) that we could be
contending with.

The second change is to add a bit more logging that allows us to monitor
the amount of work these jobs are doing. This will allow us to see if
these jobs are performing an excessive amount of work.

Finally, I moved these LaunchJobs to Swift. Mostly because the ObjC
implementations were block based. The additional code was going to
indent things further and our linter aggressiely indents blocks to begin
with. Moving this to Swift is much more readable.
This commit is contained in:
Michelle Linington 2022-01-27 18:55:17 -08:00 committed by Martin Böttcher
parent e417c1c184
commit 97409f3295
14 changed files with 205 additions and 263 deletions

View File

@ -3,6 +3,7 @@
//
import Foundation
import SignalServiceKit
@objc
public class LaunchJobs: NSObject {
@ -54,16 +55,24 @@ public class LaunchJobs: NSObject {
Logger.verbose("Starting.")
DispatchQueue.global().async {
// Getting this work done ASAP is super high priority, since we won't finish launching until
// the completion block is fired.
//
// Originally, I considered making this all synchronous on the main thread (there's no UI to
// interrupt anyway) but I figured that in the absolute *worst* case where this work takes ~15s
// we risk getting watchdogged by SpringBoard.
//
// So instead, we'll use a user interactive queue and hope that's good enough.
DispatchQueue.sharedUserInteractive.async {
// Mark all "attempting out" messages as "unsent", i.e. any messages that were not successfully
// sent before the app exited should be marked as failures.
OWSFailedMessagesJob().runSync()
FailedMessagesJob().runSync()
// Mark all "incomplete" calls as missed, e.g. any incoming or outgoing calls that were not
// connected, failed or hung up before the app existed should be marked as missed.
OWSIncompleteCallsJob().runSync()
IncompleteCallsJob().runSync()
// Mark all "downloading" attachments as "failed", i.e. any incoming attachments that were not
// successfully downloaded before the app exited should be marked as failures.
OWSFailedAttachmentDownloadsJob().runSync()
FailedAttachmentDownloadsJob().runSync()
// Kick off a low priority trim of the MSL
// This will reschedule itself on a background queue ~24h or so

View File

@ -18,7 +18,7 @@ typedef NS_ENUM(NSUInteger, TSAttachmentPointerType) {
TSAttachmentPointerTypeRestoring = 2,
};
typedef NS_ENUM(NSUInteger, TSAttachmentPointerState) {
typedef NS_CLOSED_ENUM(NSUInteger, TSAttachmentPointerState) {
TSAttachmentPointerStateEnqueued = 0,
TSAttachmentPointerStateDownloading = 1,
TSAttachmentPointerStateFailed = 2,

View File

@ -0,0 +1,60 @@
//
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
//
import Foundation
public class FailedAttachmentDownloadsJob: Dependencies {
/// Used for logging the total number of attachments modified
private var count: UInt = 0
public init() {}
public func runSync() {
databaseStorage.write { writeTx in
AttachmentFinder.unfailedAttachmentPointerIds(transaction: writeTx).forEach { attachmentId in
// Since we can't directly mutate the enumerated attachments, we store only their ids in hopes
// of saving a little memory and then enumerate the (larger) TSAttachment objects one at a time.
autoreleasepool {
updateAttachmentPointerIfNecessary(attachmentId, transaction: writeTx)
}
}
}
Logger.info("Finished job. Marked \(count) in-progress attachments as failed")
}
public func updateAttachmentPointerIfNecessary(_ uniqueId: String, transaction writeTx: SDSAnyWriteTransaction) {
// Preconditions: Must be a valid attachment pointer that hasn't failed
guard let attachment = TSAttachmentPointer.anyFetchAttachmentPointer(
uniqueId: uniqueId,
transaction: writeTx
) else {
owsFailDebug("Missing attachment with id: \(uniqueId)")
return
}
switch attachment.state {
case .enqueued, .downloading:
attachment.updateAttachmentPointerState(.failed, transaction: writeTx)
count += 1
switch count {
case ...3:
Logger.info("marked attachment pointer as failed: \(attachment.uniqueId)")
case 4:
Logger.info("eliding logs for further attachment pointers. final count will be reported once complete.")
default:
break
}
case .pendingMessageRequest:
// Do nothing. We don't want to mark this attachment as failed.
// It will be updated when the message request is resolved.
break
case .pendingManualDownload:
// Do nothing. We don't want to mark this attachment as failed.
break
case .failed:
// This should not have been returned from `unfailedAttachmentPointerIds`
owsFailDebug("Attachment has unexpected state \(attachment.uniqueId).");
}
}
}

View File

@ -0,0 +1,54 @@
//
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
//
import Foundation
public class FailedMessagesJob: Dependencies {
/// Used for logging the total number of messages modified
private var count: UInt = 0
public init() {}
public func runSync() {
databaseStorage.write { writeTx in
InteractionFinder.attemptingOutInteractionIds(transaction: writeTx).forEach { failedInteractionId in
// Since we can't directly mutate the enumerated "attempting out" expired messages, we store
// only their ids in hopes of saving a little memory and then enumerate the (larger)
// TSMessage objects one at a time.
autoreleasepool {
updateFailedMessageIfNecessary(failedInteractionId, transaction: writeTx)
}
}
}
}
public func updateFailedMessageIfNecessary(_ uniqueId: String, transaction writeTx: SDSAnyWriteTransaction) {
// Preconditions
guard let message = TSOutgoingMessage.anyFetchOutgoingMessage(uniqueId: uniqueId, transaction: writeTx) else {
owsFailDebug("Missing interaction with id: \(uniqueId)")
return
}
guard message.messageState == .sending else {
owsFailDebug("Refusing to mark as unsent message \(message.timestamp) with state: \(message.messageState)")
return
}
// Update
message.updateWithAllSendingRecipientsMarkedAsFailed(with: writeTx)
count += 1
// Log if appropriate
switch count {
case ...3:
Logger.info("marking message as unsent: \(message.uniqueId) \(message.timestamp)")
case 4:
Logger.info("eliding logs for further unsent messages. final update count will be reported once complete.")
default:
break
}
// Postcondition
owsAssertDebug(message.messageState == .failed)
}
}

View File

@ -0,0 +1,74 @@
//
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
//
import Foundation
public class IncompleteCallsJob: Dependencies {
private var count: UInt = 0
private let cutoffTimestamp: UInt64
public init() {
if CurrentAppContext().appLaunchTime.ows_millisecondsSince1970 > 0 {
cutoffTimestamp = CurrentAppContext().appLaunchTime.ows_millisecondsSince1970
} else {
owsFailDebug("Invalid app launch time")
cutoffTimestamp = Date.ows_millisecondTimestamp()
}
}
public func runSync() {
databaseStorage.write { writeTx in
InteractionFinder.incompleteCallIds(transaction: writeTx).forEach { incompleteCallId in
// Since we can't directly mutate the enumerated "incomplete" calls, we store only their ids in hopes
// of saving a little memory and then enumerate the (larger) TSCall objects one at a time.
autoreleasepool {
updateIncompleteCallIfNecessary(incompleteCallId, transaction: writeTx)
}
}
}
Logger.info("Finished job. Updated \(count) incomplete calls")
}
public func updateIncompleteCallIfNecessary(_ uniqueId: String, transaction writeTx: SDSAnyWriteTransaction) {
// Preconditions: Must be a valid call that started before the app launched.
guard let call = TSCall.anyFetchCall(uniqueId: uniqueId, transaction: writeTx) else {
owsFailDebug("Missing call with id: \(uniqueId)")
return
}
guard call.timestamp < cutoffTimestamp else {
Logger.info("Ignoring new call: \(call.uniqueId)")
return
}
// Update
let targetCallType: RPRecentCallType
switch call.callType {
case .outgoingIncomplete:
targetCallType = .outgoingMissed
case .incomingIncomplete:
targetCallType = .incomingMissed
default:
owsFailDebug("Call has unexpected type: \(call.callType)")
return
}
call.updateCallType(targetCallType, transaction: writeTx)
count += 1
// Log if appropriate
switch count {
case ...3:
Logger.info("marked call as missed: \(call.uniqueId) \(call.timestamp)")
case 4:
Logger.info("eliding logs for further incomplete calls. final update count will be reported once complete.")
default:
break
}
// Postcondition: Should be some kind of missed call
let validFinalStates: [RPRecentCallType] = [.incomingMissed, .outgoingMissed]
owsAssertDebug(validFinalStates.contains(call.callType))
}
}

View File

@ -268,7 +268,7 @@ NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(grdbId:uniqueId:receivedAtTimestamp
transaction:(SDSAnyWriteTransaction *)transaction;
// On app launch, all "sending" recipients should be marked as "failed".
- (void)updateWithAllSendingRecipientsMarkedAsFailedWithTansaction:(SDSAnyWriteTransaction *)transaction;
- (void)updateWithAllSendingRecipientsMarkedAsFailedWithTransaction:(SDSAnyWriteTransaction *)transaction;
- (BOOL)hasFailedRecipients;

View File

@ -619,7 +619,7 @@ NSUInteger const TSOutgoingMessageSchemaVersion = 1;
}];
}
- (void)updateWithAllSendingRecipientsMarkedAsFailedWithTansaction:(SDSAnyWriteTransaction *)transaction
- (void)updateWithAllSendingRecipientsMarkedAsFailedWithTransaction:(SDSAnyWriteTransaction *)transaction
{
OWSAssertDebug(transaction);

View File

@ -1,13 +0,0 @@
//
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
@interface OWSFailedAttachmentDownloadsJob : NSObject
- (void)runSync;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,73 +0,0 @@
//
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
#import "OWSFailedAttachmentDownloadsJob.h"
#import "TSAttachmentPointer.h"
#import <SignalServiceKit/SignalServiceKit-Swift.h>
NS_ASSUME_NONNULL_BEGIN
@implementation OWSFailedAttachmentDownloadsJob
- (void)enumerateAttemptingOutAttachmentsWithBlock:(void (^_Nonnull)(TSAttachmentPointer *attachment))block
transaction:(SDSAnyReadTransaction *)transaction
{
OWSAssertDebug(transaction);
// Since we can't directly mutate the enumerated attachments, we store only their ids in hopes
// of saving a little memory and then enumerate the (larger) TSAttachment objects one at a time.
NSArray<NSString *> *attachmentIds = [AttachmentFinder unfailedAttachmentPointerIdsWithTransaction:transaction];
for (NSString *attachmentId in attachmentIds) {
TSAttachmentPointer *_Nullable attachment =
[TSAttachmentPointer anyFetchAttachmentPointerWithUniqueId:attachmentId transaction:transaction];
if (attachment == nil) {
OWSFailDebug(@"Missing attachment.");
continue;
}
block(attachment);
}
}
- (void)runSync
{
__block uint count = 0;
DatabaseStorageWrite(
self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) {
[self enumerateAttemptingOutAttachmentsWithBlock:^(TSAttachmentPointer *attachment) {
// sanity check
if (attachment.state == TSAttachmentPointerStateFailed) {
OWSFailDebug(@"Attachment has unexpected state.");
return;
}
switch (attachment.state) {
case TSAttachmentPointerStateFailed:
OWSFailDebug(@"Attachment has unexpected state.");
break;
case TSAttachmentPointerStatePendingMessageRequest:
// Do nothing. We don't want to mark this attachment as failed.
// It will be updated when the message request is resolved.
break;
case TSAttachmentPointerStateEnqueued:
case TSAttachmentPointerStateDownloading:
[attachment updateWithAttachmentPointerState:TSAttachmentPointerStateFailed
transaction:transaction];
count++;
return;
case TSAttachmentPointerStatePendingManualDownload:
// Do nothing. We don't want to mark this attachment as failed.
break;
}
}
transaction:transaction];
});
if (count > 0) {
OWSLogDebug(@"Marked %u attachments as failed", count);
}
}
@end
NS_ASSUME_NONNULL_END

View File

@ -1,13 +0,0 @@
//
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
@interface OWSFailedMessagesJob : NSObject
- (void)runSync;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,67 +0,0 @@
//
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
#import "OWSFailedMessagesJob.h"
#import "TSMessage.h"
#import "TSOutgoingMessage.h"
#import <SignalServiceKit/SignalServiceKit-Swift.h>
NS_ASSUME_NONNULL_BEGIN
@implementation OWSFailedMessagesJob
- (NSArray<NSString *> *)fetchAttemptingOutMessageIdsWithTransaction:(SDSAnyReadTransaction *)transaction
{
OWSAssertDebug(transaction);
return [InteractionFinder attemptingOutInteractionIdsWithTransaction:transaction];
}
- (void)enumerateAttemptingOutMessagesWithBlock:(void (^_Nonnull)(TSOutgoingMessage *message))block
transaction:(SDSAnyReadTransaction *)transaction
{
OWSAssertDebug(transaction);
// Since we can't directly mutate the enumerated "attempting out" expired messages, we store only their ids in hopes
// of saving a little memory and then enumerate the (larger) TSMessage objects one at a time.
for (NSString *expiredMessageId in [self fetchAttemptingOutMessageIdsWithTransaction:transaction]) {
TSOutgoingMessage *_Nullable message =
[TSOutgoingMessage anyFetchOutgoingMessageWithUniqueId:expiredMessageId transaction:transaction];
if (message == nil) {
OWSFailDebug(@"Missing interaction.");
continue;
}
block(message);
}
}
- (void)runSync
{
__block uint count = 0;
DatabaseStorageWrite(
self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) {
[self enumerateAttemptingOutMessagesWithBlock:^(TSOutgoingMessage *message) {
// sanity check
OWSAssertDebug(message.messageState == TSOutgoingMessageStateSending);
if (message.messageState != TSOutgoingMessageStateSending) {
OWSLogError(@"Refusing to mark as unsent message with state: %d", (int)message.messageState);
return;
}
OWSLogDebug(@"marking message as unsent: %@", message.uniqueId);
[message updateWithAllSendingRecipientsMarkedAsFailedWithTansaction:transaction];
OWSAssertDebug(message.messageState == TSOutgoingMessageStateFailed);
count++;
}
transaction:transaction];
});
OWSLogDebug(@"Marked %u messages as unsent", count);
}
@end
NS_ASSUME_NONNULL_END

View File

@ -1,13 +0,0 @@
//
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
@interface OWSIncompleteCallsJob : NSObject
- (void)runSync;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,76 +0,0 @@
//
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
#import "OWSIncompleteCallsJob.h"
#import "AppContext.h"
#import "TSCall.h"
#import <SignalCoreKit/NSDate+OWS.h>
#import <SignalServiceKit/SignalServiceKit-Swift.h>
NS_ASSUME_NONNULL_BEGIN
@implementation OWSIncompleteCallsJob
- (NSArray<NSString *> *)fetchIncompleteCallIdsWithTransaction:(SDSAnyWriteTransaction *)transaction
{
OWSAssertDebug(transaction);
return [InteractionFinder incompleteCallIdsWithTransaction:transaction];
}
- (void)enumerateIncompleteCallsWithBlock:(void (^)(TSCall *call))block
transaction:(SDSAnyWriteTransaction *)transaction
{
OWSAssertDebug(transaction);
// Since we can't directly mutate the enumerated "incomplete" calls, we store only their ids in hopes
// of saving a little memory and then enumerate the (larger) TSCall objects one at a time.
for (NSString *callId in [self fetchIncompleteCallIdsWithTransaction:transaction]) {
TSCall *_Nullable call = [TSCall anyFetchCallWithUniqueId:callId transaction:transaction];
if (call == nil) {
OWSFailDebug(@"Missing call.");
continue;
}
block(call);
}
}
- (void)runSync
{
__block uint count = 0;
OWSAssertDebug(CurrentAppContext().appLaunchTime);
uint64_t cutoffTimestamp = [NSDate ows_millisecondsSince1970ForDate:CurrentAppContext().appLaunchTime];
DatabaseStorageWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) {
[self
enumerateIncompleteCallsWithBlock:^(TSCall *call) {
if (call.timestamp > cutoffTimestamp) {
OWSLogInfo(@"ignoring new call: %@", call.uniqueId);
return;
}
if (call.callType == RPRecentCallTypeOutgoingIncomplete) {
OWSLogDebug(@"marking call as missed: %@", call.uniqueId);
[call updateCallType:RPRecentCallTypeOutgoingMissed transaction:transaction];
OWSAssertDebug(call.callType == RPRecentCallTypeOutgoingMissed);
} else if (call.callType == RPRecentCallTypeIncomingIncomplete) {
OWSLogDebug(@"marking call as missed: %@", call.uniqueId);
[call updateCallType:RPRecentCallTypeIncomingMissed transaction:transaction];
OWSAssertDebug(call.callType == RPRecentCallTypeIncomingMissed);
} else {
OWSFailDebug(@"call has unexpected call type: %@", NSStringFromCallType(call.callType));
return;
}
count++;
}
transaction:transaction];
});
OWSLogInfo(@"Marked %u calls as missed", count);
}
@end
NS_ASSUME_NONNULL_END

View File

@ -400,7 +400,7 @@ extension SharingThreadPickerViewController {
self.databaseStorage.write { transaction in
for message in self.outgoingMessages {
// If we sent the message to anyone, mark it as failed
message.updateWithAllSendingRecipientsMarkedAsFailed(withTansaction: transaction)
message.updateWithAllSendingRecipientsMarkedAsFailed(with: transaction)
}
}
self.shareViewDelegate?.shareViewWasCancelled()