Skip to content

feat: reduce HMR crashes in Preview and recover properly after them #5098

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

Merged
merged 4 commits into from
Nov 21, 2019
Merged
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
125 changes: 91 additions & 34 deletions lib/controllers/preview-app-controller.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Device, FilesPayload } from "nativescript-preview-sdk";
import { TrackActionNames, PREPARE_READY_EVENT_NAME } from "../constants";
import { PrepareController } from "./prepare-controller";
import { Device, FilesPayload } from "nativescript-preview-sdk";
import { performanceLog } from "../common/decorators";
import { stringify } from "../common/helpers";
import { stringify, deferPromise } from "../common/helpers";
import { HmrConstants } from "../common/constants";
import { EventEmitter } from "events";
import { PrepareDataService } from "../services/prepare-data-service";
Expand All @@ -11,7 +11,14 @@ import { PreviewAppLiveSyncEvents } from "../services/livesync/playground/previe
export class PreviewAppController extends EventEmitter implements IPreviewAppController {
private prepareReadyEventHandler: any = null;
private deviceInitializationPromise: IDictionary<boolean> = {};
private promise = Promise.resolve();
private devicesLiveSyncChain: IDictionary<Promise<void>> = {};
private devicesCanExecuteHmr: IDictionary<boolean> = {};
// holds HMR files per device in order to execute batch upload on fast updates
private devicesHmrFiles: IDictionary<string[]> = {};
// holds app files per device in order to execute batch upload on fast updates on failed HMR or --no-hmr
private devicesAppFiles: IDictionary<string[]> = {};
// holds the current HMR hash per device in order to watch the proper hash status on fast updates
private devicesCurrentHmrHash: IDictionary<string> = {};

constructor(
private $analyticsService: IAnalyticsService,
Expand Down Expand Up @@ -89,6 +96,7 @@ export class PreviewAppController extends EventEmitter implements IPreviewAppCon

if (data.useHotModuleReload) {
this.$hmrStatusService.attachToHmrStatusEvent();
this.devicesCanExecuteHmr[device.id] = true;
}

await this.$previewAppPluginsService.comparePluginsOnDevice(data, device);
Expand All @@ -109,13 +117,13 @@ export class PreviewAppController extends EventEmitter implements IPreviewAppCon
await this.$prepareController.prepare(prepareData);

try {
const payloads = await this.getInitialFilesForPlatformSafe(data, device.platform);
const payloads = await this.getInitialFilesForDeviceSafe(data, device);
return payloads;
} finally {
this.deviceInitializationPromise[device.id] = null;
}
} catch (error) {
this.$logger.trace(`Error while sending files on device ${device && device.id}. Error is`, error);
this.$logger.trace(`Error while sending files on device '${device && device.id}'. Error is`, error);
this.emit(PreviewAppLiveSyncEvents.PREVIEW_APP_LIVE_SYNC_ERROR, {
error,
data,
Expand All @@ -129,52 +137,101 @@ export class PreviewAppController extends EventEmitter implements IPreviewAppCon

@performanceLog()
private async handlePrepareReadyEvent(data: IPreviewAppLiveSyncData, currentPrepareData: IFilesChangeEventData) {
await this.promise
.then(async () => {
const { hmrData, files, platform } = currentPrepareData;
const platformHmrData = _.cloneDeep(hmrData);

this.promise = this.syncFilesForPlatformSafe(data, { filesToSync: files }, platform);
await this.promise;

if (data.useHotModuleReload && platformHmrData.hash) {
const devices = this.$previewDevicesService.getDevicesForPlatform(platform);

await Promise.all(_.map(devices, async (previewDevice: Device) => {
const status = await this.$hmrStatusService.getHmrStatus(previewDevice.id, platformHmrData.hash);
if (status === HmrConstants.HMR_ERROR_STATUS) {
const originalUseHotModuleReload = data.useHotModuleReload;
data.useHotModuleReload = false;
await this.syncFilesForPlatformSafe(data, { filesToSync: platformHmrData.fallbackFiles }, platform, previewDevice.id);
data.useHotModuleReload = originalUseHotModuleReload;
}
}));
const { hmrData, files, platform } = currentPrepareData;
const platformHmrData = _.cloneDeep(hmrData);
const connectedDevices = this.$previewDevicesService.getDevicesForPlatform(platform);
if (!connectedDevices || !connectedDevices.length) {
this.$logger.warn(`Unable to find any connected devices for platform '${platform}'. In order to execute live sync, open your Preview app and optionally re-scan the QR code using the Playground app.`);
return;
}

await Promise.all(_.map(connectedDevices, async (device) => {
const previousSync = this.devicesLiveSyncChain[device.id] || Promise.resolve();
const currentSyncDeferPromise = deferPromise<void>();
this.devicesLiveSyncChain[device.id] = currentSyncDeferPromise.promise;
this.devicesCurrentHmrHash[device.id] = this.devicesCurrentHmrHash[device.id] || platformHmrData.hash;
if (data.useHotModuleReload) {
this.devicesHmrFiles[device.id] = this.devicesHmrFiles[device.id] || [];
this.devicesHmrFiles[device.id].push(...files);
this.devicesAppFiles[device.id] = platformHmrData.fallbackFiles;
} else {
this.devicesHmrFiles[device.id] = [];
this.devicesAppFiles[device.id] = files;
}

await previousSync;

try {
let canExecuteHmrSync = false;
const hmrHash = this.devicesCurrentHmrHash[device.id];
this.devicesCurrentHmrHash[device.id] = null;
if (data.useHotModuleReload && hmrHash) {
if (this.devicesCanExecuteHmr[device.id]) {
canExecuteHmrSync = true;
}
}
});

const filesToSync = canExecuteHmrSync ? this.devicesHmrFiles[device.id] : this.devicesAppFiles[device.id];
if (!filesToSync || !filesToSync.length) {
this.$logger.info(`Skipping files sync for device ${this.getDeviceDisplayName(device)}. The changes are already batch transferred in a previous sync.`);
currentSyncDeferPromise.resolve();
return;
}

this.devicesHmrFiles[device.id] = [];
this.devicesAppFiles[device.id] = [];
if (canExecuteHmrSync) {
this.$hmrStatusService.watchHmrStatus(device.id, hmrHash);
await this.syncFilesForPlatformSafe(data, { filesToSync }, platform, device);
const status = await this.$hmrStatusService.getHmrStatus(device.id, hmrHash);
if (!status) {
this.devicesCanExecuteHmr[device.id] = false;
this.$logger.warn(`Unable to get LiveSync status from the Preview app for device ${this.getDeviceDisplayName(device)}. Ensure the app is running in order to sync changes.`);
} else {
this.devicesCanExecuteHmr[device.id] = status === HmrConstants.HMR_SUCCESS_STATUS;
}
} else {
const noHmrData = _.assign({}, data, { useHotModuleReload: false });
await this.syncFilesForPlatformSafe(noHmrData, { filesToSync }, platform, device);
this.devicesCanExecuteHmr[device.id] = true;
}
currentSyncDeferPromise.resolve();
} catch (e) {
currentSyncDeferPromise.resolve();
}
}));
}

private getDeviceDisplayName(device: Device) {
return `${device.name} (${device.id})`.cyan;
}

private async getInitialFilesForPlatformSafe(data: IPreviewAppLiveSyncData, platform: string): Promise<FilesPayload> {
this.$logger.info(`Start sending initial files for platform ${platform}.`);
private async getInitialFilesForDeviceSafe(data: IPreviewAppLiveSyncData, device: Device): Promise<FilesPayload> {
const platform = device.platform;
this.$logger.info(`Start sending initial files for device ${this.getDeviceDisplayName(device)}.`);

try {
const payloads = this.$previewAppFilesService.getInitialFilesPayload(data, platform);
this.$logger.info(`Successfully sent initial files for platform ${platform}.`);
this.$logger.info(`Successfully sent initial files for device ${this.getDeviceDisplayName(device)}.`);
return payloads;
} catch (err) {
this.$logger.warn(`Unable to apply changes for platform ${platform}. Error is: ${err}, ${stringify(err)}`);
this.$logger.warn(`Unable to apply changes for device ${this.getDeviceDisplayName(device)}. Error is: ${err}, ${stringify(err)}`);
}
}

private async syncFilesForPlatformSafe(data: IPreviewAppLiveSyncData, filesData: IPreviewAppFilesData, platform: string, deviceId?: string): Promise<void> {
private async syncFilesForPlatformSafe(data: IPreviewAppLiveSyncData, filesData: IPreviewAppFilesData, platform: string, device: Device): Promise<void> {
const deviceId = device && device.id || "";

try {
const payloads = this.$previewAppFilesService.getFilesPayload(data, filesData, platform);
payloads.deviceId = deviceId;
if (payloads && payloads.files && payloads.files.length) {
this.$logger.info(`Start syncing changes for platform ${platform}.`);
this.$logger.info(`Start syncing changes for device ${this.getDeviceDisplayName(device)}.`);
await this.$previewSdkService.applyChanges(payloads);
this.$logger.info(`Successfully synced ${payloads.files.map(filePayload => filePayload.file.yellow)} for platform ${platform}.`);
this.$logger.info(`Successfully synced '${payloads.files.map(filePayload => filePayload.file.yellow)}' for device ${this.getDeviceDisplayName(device)}.`);
}
} catch (error) {
this.$logger.warn(`Unable to apply changes for platform ${platform}. Error is: ${error}, ${JSON.stringify(error, null, 2)}.`);
this.$logger.warn(`Unable to apply changes for device ${this.getDeviceDisplayName(device)}. Error is: ${error}, ${JSON.stringify(error, null, 2)}.`);
this.emit(PreviewAppLiveSyncEvents.PREVIEW_APP_LIVE_SYNC_ERROR, {
error,
data,
Expand Down
3 changes: 2 additions & 1 deletion lib/controllers/run-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,8 @@ export class RunController extends EventEmitter implements IRunController {

if (!liveSyncResultInfo.didRecover && isInHMRMode) {
const status = await this.$hmrStatusService.getHmrStatus(device.deviceInfo.identifier, data.hmrData.hash);
if (status === HmrConstants.HMR_ERROR_STATUS) {
// error or timeout
if (status !== HmrConstants.HMR_SUCCESS_STATUS) {
watchInfo.filesToSync = data.hmrData.fallbackFiles;
liveSyncResultInfo = await platformLiveSyncService.liveSyncWatchAction(device, watchInfo);
// We want to force a restart of the application.
Expand Down
2 changes: 1 addition & 1 deletion lib/definitions/preview-app-livesync.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ declare global {

interface IPreviewAppLiveSyncData extends IProjectDir, IHasUseHotModuleReloadOption, IEnvOptions { }

interface IPreviewSdkService extends EventEmitter {
interface IPreviewSdkService {
getQrCodeUrl(options: IGetQrCodeUrlOptions): string;
initialize(projectDir: string, getInitialFiles: (device: Device) => Promise<FilesPayload>): void;
applyChanges(filesPayload: FilesPayload): Promise<void>;
Expand Down
4 changes: 1 addition & 3 deletions lib/services/livesync/playground/preview-sdk-service.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { MessagingService, Config, Device, DeviceConnectedMessage, SdkCallbacks, ConnectedDevices, FilesPayload } from "nativescript-preview-sdk";
import { EventEmitter } from "events";
const pako = require("pako");

export class PreviewSdkService extends EventEmitter implements IPreviewSdkService {
export class PreviewSdkService implements IPreviewSdkService {
private static MAX_FILES_UPLOAD_BYTE_LENGTH = 15 * 1024 * 1024; // In MBs
private messagingService: MessagingService = null;
private instanceId: string = null;
Expand All @@ -13,7 +12,6 @@ export class PreviewSdkService extends EventEmitter implements IPreviewSdkServic
private $previewDevicesService: IPreviewDevicesService,
private $previewAppLogProvider: IPreviewAppLogProvider,
private $previewSchemaService: IPreviewSchemaService) {
super();
}

public getQrCodeUrl(options: IGetQrCodeUrlOptions): string {
Expand Down
8 changes: 5 additions & 3 deletions lib/services/log-parser-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export class LogParserService extends EventEmitter implements ILogParserService

constructor(private $deviceLogProvider: Mobile.IDeviceLogProvider,
private $errors: IErrors,
private $previewSdkService: IPreviewSdkService) {
private $previewAppLogProvider: IPreviewAppLogProvider) {
super();
}

Expand All @@ -23,10 +23,12 @@ export class LogParserService extends EventEmitter implements ILogParserService
@cache()
private startParsingLogCore(): void {
this.$deviceLogProvider.on(DEVICE_LOG_EVENT_NAME, this.processDeviceLogResponse.bind(this));
this.$previewSdkService.on(DEVICE_LOG_EVENT_NAME, this.processDeviceLogResponse.bind(this));
this.$previewAppLogProvider.on(DEVICE_LOG_EVENT_NAME, (deviceId: string, message: string) => {
this.processDeviceLogResponse(message, deviceId);
});
}

private processDeviceLogResponse(message: string, deviceIdentifier: string, devicePlatform: string) {
private processDeviceLogResponse(message: string, deviceIdentifier: string, devicePlatform?: string) {
const lines = message.split("\n");
_.forEach(lines, line => {
_.forEach(this.parseRules, parseRule => {
Expand Down
1 change: 1 addition & 0 deletions test/services/ios-debugger-port-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const device = <Mobile.IDevice>{
function createTestInjector() {
const injector = new Yok();

injector.register("previewAppLogProvider", { on: () => ({}) });
injector.register("devicePlatformsConstants", DevicePlatformsConstants);
injector.register("deviceLogProvider", DeveiceLogProviderMock);
injector.register("errors", ErrorsStub);
Expand Down
1 change: 1 addition & 0 deletions test/services/log-parser-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class DeveiceLogProviderMock extends EventEmitter {

function createTestInjector() {
const injector = new Yok();
injector.register("previewAppLogProvider", { on: () => ({}) });
injector.register("deviceLogProvider", DeveiceLogProviderMock);
injector.register("previewSdkService", {
on: () => ({})
Expand Down