utils.ts 10.8 KB
Newer Older
liuzhe-lz's avatar
liuzhe-lz committed
1
2
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
Deshui Yu's avatar
Deshui Yu committed
3

4
import assert from 'assert';
Deshui Yu's avatar
Deshui Yu committed
5
import { randomBytes } from 'crypto';
6
7
import cpp from 'child-process-promise';
import cp from 'child_process';
8
import { ChildProcess, spawn, StdioOptions } from 'child_process';
9
10
11
12
13
import dgram from 'dgram';
import fs from 'fs';
import net from 'net';
import os from 'os';
import path from 'path';
liuzhe-lz's avatar
liuzhe-lz committed
14
import * as timersPromises from 'timers/promises';
Deshui Yu's avatar
Deshui Yu committed
15
16
17
18
import { Deferred } from 'ts-deferred';
import { Container } from 'typescript-ioc';

import { Database, DataStore } from './datastore';
19
20
import globals from './globals';
import { resetGlobals } from './globals/unittest';  // TODO: this file should not contain unittest helpers
21
import { ExperimentConfig, Manager } from './manager';
QuanluZhang's avatar
QuanluZhang committed
22
import { HyperParameters, TrainingService, TrialJobStatus } from './trainingService';
Deshui Yu's avatar
Deshui Yu committed
23

24
function getExperimentRootDir(): string {
25
    return globals.paths.experimentRoot;
Deshui Yu's avatar
Deshui Yu committed
26
27
}

28
function getLogDir(): string {
29
    return globals.paths.logDirectory;
Deshui Yu's avatar
Deshui Yu committed
30
31
}

32
function getLogLevel(): string {
33
    return globals.args.logLevel;
34
35
}

Deshui Yu's avatar
Deshui Yu committed
36
37
38
39
function getDefaultDatabaseDir(): string {
    return path.join(getExperimentRootDir(), 'db');
}

QuanluZhang's avatar
QuanluZhang committed
40
41
42
43
function getCheckpointDir(): string {
    return path.join(getExperimentRootDir(), 'checkpoint');
}

liuzhe-lz's avatar
liuzhe-lz committed
44
45
async function mkDirP(dirPath: string): Promise<void> {
    await fs.promises.mkdir(dirPath, { recursive: true });
Deshui Yu's avatar
Deshui Yu committed
46
47
48
}

function mkDirPSync(dirPath: string): void {
liuzhe-lz's avatar
liuzhe-lz committed
49
    fs.mkdirSync(dirPath, { recursive: true });
Deshui Yu's avatar
Deshui Yu committed
50
51
}

liuzhe-lz's avatar
liuzhe-lz committed
52
const delay = timersPromises.setTimeout;
Deshui Yu's avatar
Deshui Yu committed
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90

/**
 * Convert index to character
 * @param index index
 * @returns a mapping character
 */
function charMap(index: number): number {
    if (index < 26) {
        return index + 97;
    } else if (index < 52) {
        return index - 26 + 65;
    } else {
        return index - 52 + 48;
    }
}

/**
 * Generate a unique string by length
 * @param len length of string
 * @returns a unique string
 */
function uniqueString(len: number): string {
    if (len === 0) {
        return '';
    }
    const byteLength: number = Math.ceil((Math.log2(52) + Math.log2(62) * (len - 1)) / 8);
    let num: number = randomBytes(byteLength).reduce((a: number, b: number) => a * 256 + b, 0);
    const codes: number[] = [];
    codes.push(charMap(num % 52));
    num = Math.floor(num / 52);
    for (let i: number = 1; i < len; i++) {
        codes.push(charMap(num % 62));
        num = Math.floor(num / 62);
    }

    return String.fromCharCode(...codes);
}

91
92
93
94
function randomInt(max: number): number {
    return Math.floor(Math.random() * max);
}

95
96
97
98
99
function randomSelect<T>(a: T[]): T {
    assert(a !== undefined);

    return a[Math.floor(Math.random() * a.length)];
}
100

101
/**
102
 * Generate command line to start automl algorithm(s),
QuanluZhang's avatar
QuanluZhang committed
103
 * either start advisor or start a process which runs tuner and assessor
104
 *
chicm-ms's avatar
chicm-ms committed
105
 * @param expParams: experiment startup parameters
106
107
 *
 */
108
function getMsgDispatcherCommand(expParams: ExperimentConfig): string[] {
chicm-ms's avatar
chicm-ms committed
109
110
    const clonedParams = Object.assign({}, expParams);
    delete clonedParams.searchSpace;
111
    return [ globals.args.pythonInterpreter, '-m', 'nni', '--exp_params', Buffer.from(JSON.stringify(clonedParams)).toString('base64') ];
112
113
}

114
115
116
117
/**
 * Generate parameter file name based on HyperParameters object
 * @param hyperParameters HyperParameters instance
 */
chicm-ms's avatar
chicm-ms committed
118
function generateParamFileName(hyperParameters: HyperParameters): string {
119
120
121
    assert(hyperParameters !== undefined);
    assert(hyperParameters.index >= 0);

chicm-ms's avatar
chicm-ms committed
122
    let paramFileName: string;
123
    if (hyperParameters.index == 0) {
124
125
126
127
128
129
130
        paramFileName = 'parameter.cfg';
    } else {
        paramFileName = `parameter_${hyperParameters.index}.cfg`
    }
    return paramFileName;
}

Deshui Yu's avatar
Deshui Yu committed
131
132
133
134
135
136
137
138
139
140
/**
 * Initialize a pseudo experiment environment for unit test.
 * Must be paired with `cleanupUnitTest()`.
 */
function prepareUnitTest(): void {
    Container.snapshot(Database);
    Container.snapshot(DataStore);
    Container.snapshot(TrainingService);
    Container.snapshot(Manager);

141
    resetGlobals();
Deshui Yu's avatar
Deshui Yu committed
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161

    const sqliteFile: string = path.join(getDefaultDatabaseDir(), 'nni.sqlite');
    try {
        fs.unlinkSync(sqliteFile);
    } catch (err) {
        // file not exists, good
    }
}

/**
 * Clean up unit test pseudo experiment.
 * Must be paired with `prepareUnitTest()`.
 */
function cleanupUnitTest(): void {
    Container.restore(Manager);
    Container.restore(TrainingService);
    Container.restore(DataStore);
    Container.restore(Database);
}

162
163
let cachedIpv4Address: string | null = null;

164
/**
165
 * Get IPv4 address of current machine.
166
 */
liuzhe-lz's avatar
liuzhe-lz committed
167
async function getIPV4Address(): Promise<string> {
168
169
    if (cachedIpv4Address !== null) {
        return cachedIpv4Address;
170
    }
171

172
173
174
175
    // creates "udp connection" to a non-exist target, and get local address of the connection.
    // since udp is connectionless, this does not send actual packets.
    const socket = dgram.createSocket('udp4');
    socket.connect(1, '192.0.2.0');
liuzhe-lz's avatar
liuzhe-lz committed
176
    for (let i = 0; i < 10; i++) {  // wait the system to initialize "connection"
liuzhe-lz's avatar
liuzhe-lz committed
177
178
179
180
181
182
183
184
        await timersPromises.setTimeout(1);
        try {
            cachedIpv4Address = socket.address().address;
            socket.close();
            return cachedIpv4Address;
        } catch (error) {
            /* retry */
        }
liuzhe-lz's avatar
liuzhe-lz committed
185
    }
liuzhe-lz's avatar
liuzhe-lz committed
186

liuzhe-lz's avatar
liuzhe-lz committed
187
    cachedIpv4Address = socket.address().address;  // if it still fails, throw the error
188
189
    socket.close();
    return cachedIpv4Address;
190
191
}

QuanluZhang's avatar
QuanluZhang committed
192
193
194
195
196
197
198
/**
 * Get the status of canceled jobs according to the hint isEarlyStopped
 */
function getJobCancelStatus(isEarlyStopped: boolean): TrialJobStatus {
    return isEarlyStopped ? 'EARLY_STOPPED' : 'USER_CANCELED';
}

199
200
201
202
/**
 * Utility method to calculate file numbers under a directory, recursively
 * @param directory directory name
 */
chicm-ms's avatar
chicm-ms committed
203
function countFilesRecursively(directory: string): Promise<number> {
204
    if (!fs.existsSync(directory)) {
205
206
207
208
209
        throw Error(`Direcotory ${directory} doesn't exist`);
    }

    const deferred: Deferred<number> = new Deferred<number>();

chicm-ms's avatar
chicm-ms committed
210
    let timeoutId: NodeJS.Timer
211
    const delayTimeout: Promise<number> = new Promise((_resolve: Function, reject: Function): void => {
212
213
214
215
216
217
218
        // Set timeout and reject the promise once reach timeout (5 seconds)
        timeoutId = setTimeout(() => {
            reject(new Error(`Timeout: path ${directory} has too many files`));
        }, 5000);
    });

    let fileCount: number = -1;
219
    let cmd: string;
220
    if (process.platform === "win32") {
221
222
        cmd = `powershell "Get-ChildItem -Path ${directory} -Recurse -File | Measure-Object | %{$_.Count}"`
    } else {
223
        cmd = `find ${directory} -type f | wc -l`;
224
225
    }
    cpp.exec(cmd).then((result) => {
226
        if (result.stdout && parseInt(result.stdout)) {
227
            fileCount = parseInt(result.stdout);
228
229
230
231
232
233
234
235
        }
        deferred.resolve(fileCount);
    });
    return Promise.race([deferred.promise, delayTimeout]).finally(() => {
        clearTimeout(timeoutId);
    });
}

236
237
238
239
/**
 * get the version of current package
 */
async function getVersion(): Promise<string> {
chicm-ms's avatar
chicm-ms committed
240
    const deferred: Deferred<string> = new Deferred<string>();
241
    import(path.join(__dirname, '..', 'package.json')).then((pkg) => {
242
        deferred.resolve(pkg.version);
243
244
    }).catch(() => {
        deferred.resolve('999.0.0-developing');
245
246
    });
    return deferred.promise;
247
}
248

249
250
251
/**
 * run command as ChildProcess
 */
252
253
function getTunerProc(command: string[], stdio: StdioOptions, newCwd: string, newEnv: any, newShell: boolean = true, isDetached: boolean = false): ChildProcess {
    // FIXME: TensorBoard has no reason to use get TUNER proc
254
    if (process.platform === "win32") {
255
        newShell = false;
256
        isDetached = true;
257
    }
258
    const tunerProc: ChildProcess = spawn(command[0], command.slice(1), {
259
260
261
        stdio,
        cwd: newCwd,
        env: newEnv,
262
263
        shell: newShell,
        detached: isDetached
264
265
266
267
268
269
270
    });
    return tunerProc;
}

/**
 * judge whether the process is alive
 */
Yuge Zhang's avatar
Yuge Zhang committed
271
async function isAlive(pid: any): Promise<boolean> {
chicm-ms's avatar
chicm-ms committed
272
    const deferred: Deferred<boolean> = new Deferred<boolean>();
273
    let alive: boolean = false;
Yuge Zhang's avatar
Yuge Zhang committed
274
    if (process.platform === 'win32') {
275
276
277
278
279
280
281
        try {
            const str = cp.execSync(`powershell.exe Get-Process -Id ${pid} -ErrorAction SilentlyContinue`).toString();
            if (str) {
                alive = true;
            }
        }
        catch (error) {
chicm-ms's avatar
chicm-ms committed
282
            //ignore
283
284
        }
    }
Yuge Zhang's avatar
Yuge Zhang committed
285
    else {
286
287
288
289
290
291
292
293
294
295
296
297
        try {
            await cpp.exec(`kill -0 ${pid}`);
            alive = true;
        } catch (error) {
            //ignore
        }
    }
    deferred.resolve(alive);
    return deferred.promise;
}

/**
298
 * kill process
299
 */
Yuge Zhang's avatar
Yuge Zhang committed
300
async function killPid(pid: any): Promise<void> {
chicm-ms's avatar
chicm-ms committed
301
    const deferred: Deferred<void> = new Deferred<void>();
302
303
    try {
        if (process.platform === "win32") {
Yuge Zhang's avatar
Yuge Zhang committed
304
            await cpp.exec(`cmd.exe /c taskkill /PID ${pid} /F`);
305
        }
306
        else {
307
308
309
310
311
312
313
314
315
            await cpp.exec(`kill -9 ${pid}`);
        }
    } catch (error) {
        // pid does not exist, do nothing here
    }
    deferred.resolve();
    return deferred.promise;
}

316
function getNewLine(): string {
317
318
319
    if (process.platform === "win32") {
        return "\r\n";
    }
320
    else {
321
322
323
324
        return "\n";
    }
}

325
326
/**
 * Use '/' to join path instead of '\' for all kinds of platform
327
 * @param path
328
329
330
331
332
333
334
 */
function unixPathJoin(...paths: any[]): string {
    const dir: string = paths.filter((path: any) => path !== '').join('/');
    if (dir === '') return '.';
    return dir;
}

J-shang's avatar
J-shang committed
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
async function isPortOpen(host: string, port: number): Promise<boolean> {
    return new Promise<boolean>((resolve, reject) => {
        try{
            const stream = net.createConnection(port, host);
            const id = setTimeout(() => {
                stream.destroy();
                resolve(false);
            }, 1000);

            stream.on('connect', () => {
                clearTimeout(id);
                stream.destroy();
                resolve(true);
            });

            stream.on('error', () => {
                clearTimeout(id);
                stream.destroy();
                resolve(false);
            });
        } catch (error) {
            reject(error);
        }
    });
}

async function getFreePort(host: string, start: number, end: number): Promise<number> {
    if (start > end) {
        throw new Error(`no more free port`);
    }
    if (await isPortOpen(host, start)) {
        return await getFreePort(host, start + 1, end);
    } else {
        return start;
    }
}

372
373
374
375
376
export function importModule(modulePath: string): any {
    module.paths.unshift(path.dirname(modulePath));
    return require(path.basename(modulePath));
}

377
export {
378
379
    countFilesRecursively, generateParamFileName, getMsgDispatcherCommand, getCheckpointDir,
    getLogDir, getExperimentRootDir, getJobCancelStatus, getDefaultDatabaseDir, getIPV4Address, unixPathJoin, getFreePort, isPortOpen,
380
    mkDirP, mkDirPSync, delay, prepareUnitTest, cleanupUnitTest, uniqueString, randomInt, randomSelect, getLogLevel, getVersion, getTunerProc, isAlive, killPid, getNewLine
381
};