From 2dab7977f5885e061fbb48c2a7c41e9127fe8d9b Mon Sep 17 00:00:00 2001 From: rosen-vladimirov Date: Tue, 29 Jan 2019 18:15:53 +0200 Subject: [PATCH] fix: high cpu usage during livesync During LiveSync operations (i.e. `tns run ...`), CLI may keep the CPU of the machine at a constantly high level even when it is not livesyncing. This is caused by the device detection changes that we are starting, which also checks for new emulator images. It turnes out the command `avdmanager list avds` takes really high usage of CPU on some machines when there are available AVD images. To fix this behavior split the current interval on two different ones - one will check for devices, which is run on 200ms and a second one which will check for emulator images, which will run once per minute. Fix the `startDeviceDetectionInterval` method - it should just start the device detection instead of waiting for the first execution to receive results. This breaks the current behavior, so execute a single synchronous device detection before starting the interval - this way we keep the current behavior. --- .../mobile/mobile-core/devices-service.ts | 81 +++++++++++-------- .../test/unit-tests/mobile/devices-service.ts | 72 ++++++++++------- 2 files changed, 91 insertions(+), 62 deletions(-) diff --git a/lib/common/mobile/mobile-core/devices-service.ts b/lib/common/mobile/mobile-core/devices-service.ts index a9b9330578..30d04dc344 100644 --- a/lib/common/mobile/mobile-core/devices-service.ts +++ b/lib/common/mobile/mobile-core/devices-service.ts @@ -13,6 +13,7 @@ import { performanceLog } from "../../decorators"; export class DevicesService extends EventEmitter implements Mobile.IDevicesService { private static DEVICE_LOOKING_INTERVAL = 200; + private static EMULATOR_IMAGES_DETECTION_INTERVAL = 60 * 1000; private _devices: IDictionary = {}; private _availableEmulators: IDictionary = {}; private _platform: string; @@ -21,8 +22,7 @@ export class DevicesService extends EventEmitter implements Mobile.IDevicesServi private _data: Mobile.IDevicesServicesInitializationOptions; private _otherDeviceDiscoveries: Mobile.IDeviceDiscovery[] = []; private _allDeviceDiscoveries: Mobile.IDeviceDiscovery[] = []; - private deviceDetectionInterval: any; - private isDeviceDetectionIntervalInProgress: boolean; + private deviceDetectionIntervals: NodeJS.Timer[] = []; constructor(private $logger: ILogger, private $errors: IErrors, @@ -287,43 +287,51 @@ export class DevicesService extends EventEmitter implements Mobile.IDevicesServi } } - protected async startDeviceDetectionInterval(deviceInitOpts: Mobile.IDevicesServicesInitializationOptions = {}): Promise { + protected async startDeviceDetectionIntervals(deviceInitOpts: Mobile.IDevicesServicesInitializationOptions = {}): Promise { this.$processService.attachToProcessExitSignals(this, this.clearDeviceDetectionInterval); - if (this.deviceDetectionInterval) { - this.$logger.trace("Device detection interval is already started. New Interval will not be started."); + if (this.deviceDetectionIntervals.length) { + this.$logger.trace("Device detection intervals are already started. New intervals will not be started."); return; } - let isFirstExecution = true; - return new Promise((resolve, reject) => { - this.deviceDetectionInterval = setInterval(async () => { - if (this.isDeviceDetectionIntervalInProgress) { - return; - } + let isDeviceDetectionIntervalInProgress = false; + const deviceDetectionInterval = setInterval(async () => { + if (isDeviceDetectionIntervalInProgress) { + return; + } - this.isDeviceDetectionIntervalInProgress = true; + isDeviceDetectionIntervalInProgress = true; - await this.detectCurrentlyAttachedDevices(deviceInitOpts); - await this.detectCurrentlyAvailableEmulators(); + await this.detectCurrentlyAttachedDevices(deviceInitOpts); - try { - const trustedDevices = _.filter(this._devices, device => device.deviceInfo.status === constants.CONNECTED_STATUS); - await settlePromises(_.map(trustedDevices, device => device.applicationManager.checkForApplicationUpdates())); - } catch (err) { - this.$logger.trace("Error checking for application updates on devices.", err); - } + try { + const trustedDevices = _.filter(this._devices, device => device.deviceInfo.status === constants.CONNECTED_STATUS); + await settlePromises(_.map(trustedDevices, device => device.applicationManager.checkForApplicationUpdates())); + } catch (err) { + this.$logger.trace("Error checking for application updates on devices.", err); + } - if (isFirstExecution) { - isFirstExecution = false; - resolve(); - this.deviceDetectionInterval.unref(); - } + isDeviceDetectionIntervalInProgress = false; - this.isDeviceDetectionIntervalInProgress = false; + }, DevicesService.DEVICE_LOOKING_INTERVAL); - }, DevicesService.DEVICE_LOOKING_INTERVAL); - }); + deviceDetectionInterval.unref(); + this.deviceDetectionIntervals.push(deviceDetectionInterval); + + let isEmulatorDetectionIntervalRunning = false; + const emulatorDetectionInterval = setInterval(async () => { + if (isEmulatorDetectionIntervalRunning) { + return; + } + + isEmulatorDetectionIntervalRunning = true; + await this.detectCurrentlyAvailableEmulators(); + isEmulatorDetectionIntervalRunning = false; + }, DevicesService.EMULATOR_IMAGES_DETECTION_INTERVAL); + + emulatorDetectionInterval.unref(); + this.deviceDetectionIntervals.push(emulatorDetectionInterval); } /** @@ -348,14 +356,17 @@ export class DevicesService extends EventEmitter implements Mobile.IDevicesServi /** * Starts looking for running devices. All found devices are pushed to _devices variable. */ - private startLookingForDevices(deviceInitOpts?: Mobile.IDevicesServicesInitializationOptions): Promise { + private async startLookingForDevices(deviceInitOpts?: Mobile.IDevicesServicesInitializationOptions): Promise { this.$logger.trace("startLookingForDevices; platform is %s", this._platform); if (this._platform) { return this.detectCurrentlyAttachedDevices(deviceInitOpts); } - return this.startDeviceDetectionInterval(deviceInitOpts); + await this.detectCurrentlyAttachedDevices(deviceInitOpts); + await this.detectCurrentlyAvailableEmulators(); + + await this.startDeviceDetectionIntervals(deviceInitOpts); } /** @@ -690,10 +701,14 @@ export class DevicesService extends EventEmitter implements Mobile.IDevicesServi } private clearDeviceDetectionInterval(): void { - if (this.deviceDetectionInterval) { - clearInterval(this.deviceDetectionInterval); + if (this.deviceDetectionIntervals.length) { + for (const interval of this.deviceDetectionIntervals) { + clearInterval(interval); + } + + this.deviceDetectionIntervals.splice(0, this.deviceDetectionIntervals.length); } else { - this.$logger.trace("Device detection interval is not started, so it cannot be stopped."); + this.$logger.trace("Device detection intervals are not started, so it cannot be stopped."); } } diff --git a/lib/common/test/unit-tests/mobile/devices-service.ts b/lib/common/test/unit-tests/mobile/devices-service.ts index 6735332eda..fe7df258ff 100644 --- a/lib/common/test/unit-tests/mobile/devices-service.ts +++ b/lib/common/test/unit-tests/mobile/devices-service.ts @@ -28,8 +28,8 @@ class DevicesServiceInheritor extends DevicesService { return super.startEmulatorIfNecessary(data); } - public startDeviceDetectionInterval(deviceInitOpts: Mobile.IDevicesServicesInitializationOptions = {}): Promise { - return super.startDeviceDetectionInterval(deviceInitOpts); + public startDeviceDetectionIntervals(deviceInitOpts: Mobile.IDevicesServicesInitializationOptions = {}): Promise { + return super.startDeviceDetectionIntervals(deviceInitOpts); } public detectCurrentlyAttachedDevices(options?: Mobile.IDevicesServicesInitializationOptions): Promise { @@ -236,6 +236,15 @@ function resetDefaultSetInterval(): void { global.setInterval = originalSetInterval; } +async function assertOnNextTick(assertionFunction: Function): Promise { + await new Promise(resolve => { + process.nextTick(() => { + assertionFunction(); + resolve(); + }); + }); +} + describe("devicesService", () => { let counter = 0; const iOSDevice = { @@ -1184,7 +1193,7 @@ describe("devicesService", () => { }); }); - describe("startDeviceDetectionInterval", () => { + describe("startDeviceDetectionIntervals", () => { let setIntervalsCalledCount: number; beforeEach(() => { @@ -1203,7 +1212,7 @@ describe("devicesService", () => { hasStartedDeviceDetectionInterval = true; }); - await devicesService.startDeviceDetectionInterval(); + await devicesService.startDeviceDetectionIntervals(); assert.isTrue(hasStartedDeviceDetectionInterval); }); @@ -1227,11 +1236,11 @@ describe("devicesService", () => { }; }; - await devicesService.startDeviceDetectionInterval(); - await devicesService.startDeviceDetectionInterval(); - await devicesService.startDeviceDetectionInterval(); + await devicesService.startDeviceDetectionIntervals(); + await devicesService.startDeviceDetectionIntervals(); + await devicesService.startDeviceDetectionIntervals(); - assert.deepEqual(setIntervalsCalledCount, 1); + assert.deepEqual(setIntervalsCalledCount, 2); }); describe("ios devices check", () => { @@ -1248,7 +1257,7 @@ describe("devicesService", () => { hasCheckedForIosDevices = true; }; - await devicesService.startDeviceDetectionInterval(); + await devicesService.startDeviceDetectionIntervals(); assert.isTrue(hasCheckedForIosDevices); }); @@ -1256,7 +1265,7 @@ describe("devicesService", () => { it("should not throw if ios device check fails throws an exception.", async () => { ($iOSDeviceDiscovery).checkForDevices = throwErrorFunction; - await assert.isFulfilled(devicesService.startDeviceDetectionInterval()); + await assert.isFulfilled(devicesService.startDeviceDetectionIntervals()); }); }); @@ -1267,22 +1276,22 @@ describe("devicesService", () => { $androidDeviceDiscovery = testInjector.resolve("androidDeviceDiscovery"); }); - it("should check for android devices.", async () => { + it("should start interval that will check for android devices.", async () => { let hasCheckedForAndroidDevices = false; $androidDeviceDiscovery.startLookingForDevices = async (): Promise => { hasCheckedForAndroidDevices = true; }; - await devicesService.startDeviceDetectionInterval(); - - assert.isTrue(hasCheckedForAndroidDevices); + mockSetInterval(); + await devicesService.startDeviceDetectionIntervals(); + await assertOnNextTick(() => assert.isTrue(hasCheckedForAndroidDevices)); }); it("should not throw if android device check fails throws an exception.", async () => { $androidDeviceDiscovery.startLookingForDevices = throwErrorFunction; - await assert.isFulfilled(devicesService.startDeviceDetectionInterval()); + await assert.isFulfilled(devicesService.startDeviceDetectionIntervals()); }); }); @@ -1305,7 +1314,7 @@ describe("devicesService", () => { it("should not throw if ios simulator check fails throws an exception.", async () => { ($iOSSimulatorDiscovery).checkForDevices = throwErrorFunction; - await assert.isFulfilled(devicesService.startDeviceDetectionInterval()); + await assert.isFulfilled(devicesService.startDeviceDetectionIntervals()); }); }); @@ -1317,22 +1326,23 @@ describe("devicesService", () => { devicesService.addDeviceDiscovery(customDeviceDiscovery); }); - it("should check for devices.", async () => { + it("should check for devices in interval", async () => { let hasCheckedForDevices = false; customDeviceDiscovery.startLookingForDevices = async (): Promise => { hasCheckedForDevices = true; }; - await devicesService.startDeviceDetectionInterval(); + mockSetInterval(); + await devicesService.startDeviceDetectionIntervals(); - assert.isTrue(hasCheckedForDevices); + await assertOnNextTick(() => assert.isTrue(hasCheckedForDevices)); }); it("should not throw if device check fails throws an exception.", async () => { customDeviceDiscovery.startLookingForDevices = throwErrorFunction; - await assert.isFulfilled(devicesService.startDeviceDetectionInterval()); + await assert.isFulfilled(devicesService.startDeviceDetectionIntervals()); }); }); @@ -1361,26 +1371,30 @@ describe("devicesService", () => { }); it("should check for application updates for all connected devices.", async () => { - await devicesService.startDeviceDetectionInterval(); + await devicesService.startDeviceDetectionIntervals(); - assert.isTrue(hasCheckedForAndroidAppUpdates); - assert.isTrue(hasCheckedForIosAppUpdates); + await assertOnNextTick(() => { + assert.isTrue(hasCheckedForAndroidAppUpdates); + assert.isTrue(hasCheckedForIosAppUpdates); + }); }); it("should check for application updates if the check on one device throws an exception.", async () => { iOSDevice.applicationManager.checkForApplicationUpdates = throwErrorFunction; - await devicesService.startDeviceDetectionInterval(); + await devicesService.startDeviceDetectionIntervals(); - assert.isTrue(hasCheckedForAndroidAppUpdates); + await assertOnNextTick(() => assert.isTrue(hasCheckedForAndroidAppUpdates)); }); it("should check for application updates only on devices with status Connected", async () => { androidDevice.deviceInfo.status = constants.UNREACHABLE_STATUS; - await devicesService.startDeviceDetectionInterval(); + await devicesService.startDeviceDetectionIntervals(); - assert.isFalse(hasCheckedForAndroidAppUpdates); - assert.isTrue(hasCheckedForIosAppUpdates); + await assertOnNextTick(() => { + assert.isFalse(hasCheckedForAndroidAppUpdates); + assert.isTrue(hasCheckedForIosAppUpdates); + }); }); it("should not throw if all checks for application updates on all devices throw exceptions.", () => { @@ -1388,7 +1402,7 @@ describe("devicesService", () => { androidDevice.applicationManager.checkForApplicationUpdates = throwErrorFunction; const callback = () => { - devicesService.startDeviceDetectionInterval.call(devicesService); + devicesService.startDeviceDetectionIntervals.call(devicesService); }; assert.doesNotThrow(callback);