Skip to content
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

Fix perf regression #6204

Merged
merged 7 commits into from
Aug 3, 2018
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
17 changes: 16 additions & 1 deletion __tests__/commands/install/integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import * as reporters from '../../../src/reporters/index.js';
import {Install, run as install} from '../../../src/cli/commands/install.js';
import Lockfile from '../../../src/lockfile';
import * as fs from '../../../src/util/fs.js';
import * as misc from '../../../src/util/misc.js';
import {getPackageVersion, explodeLockfile, runInstall, runLink, createLockfile, run as buildRun} from '../_helpers.js';

jasmine.DEFAULT_TIMEOUT_INTERVAL = 150000;
Expand Down Expand Up @@ -111,6 +112,20 @@ test('installing a package with a renamed file should not delete it', () =>
expect(await fs.exists(`${config.cwd}/node_modules/pkg/state.js`)).toEqual(true);
}));

test("installing a new package should correctly update it, even if the files mtime didn't change", () =>
runInstall({}, 'mtime-same', async (config, reporter): Promise<void> => {
await misc.sleep(2000);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any chance we can do this manually somehow rather than prolonging the test?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm afraid not - even with the subsecond fs patch, subsecond timings still won't work on filesystems that don't support it at all (I think it might be the case on CircleCI), so we need to wait at least >1s to be sure that the mtime will change.

Another option might be to mock the fs module here, but it seemed a bit hazardous so I went with the sleep (since the tests are executed in parallel, it doesn't actually take the whole two seconds in practice).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I'm not following (or maybe this doesn't work, I haven't tested it) but won't this do the trick?
https://nodejs.org/api/fs.html#fs_fs_futimes_fd_atime_mtime_callback


const pkgJson = await fs.readJson(`${config.cwd}/package.json`);
pkgJson.dependencies['pkg'] = 'file:./pkg-b.tgz';
await fs.writeFile(`${config.cwd}/package.json`, JSON.stringify(pkgJson));

const reInstall = new Install({}, config, reporter, await Lockfile.fromDirectory(config.cwd));
await reInstall.init();

expect(await fs.readJson(`${config.cwd}/node_modules/pkg/package.json`)).toMatchObject({version: '2.0.0'});
}));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test still passes if I revert the changes (speficially in util/fs). Is this a local/os issue for me?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a test that ensures that #5723 is still fixed. #6010 was missing tests, so I wanted to be sure that the initial bug wasn't coming back after replacing the commit that was meant to fix it.

There is no test for the perf regression since I'm not sure we can make one that wouldn't be super-flaky 😞 To give you an idea, the different of install time on my machine was 6s, which could potentially come from various other sources (slow network, ...).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Legit. I was just looking for something that would fail if these changes accidentally reverted or changed somehow. Maybe as a unit test for untar-stream when it's moved and for fs?


test('properly find and save build artifacts', () =>
runInstall({}, 'artifacts-finds-and-saves', async config => {
const integrity = await fs.readJson(path.join(config.cwd, 'node_modules', constants.INTEGRITY_FILENAME));
Expand Down Expand Up @@ -217,7 +232,7 @@ test('changes the cache path when bumping the cache version', () =>
const reporter = new reporters.JSONReporter({stdout: inOut});

await cache(config, reporter, {}, ['dir']);
expect((JSON.parse(String(inOut.read())): any).data).toMatch(/[\\\/]v1[\\\/]?$/);
expect((JSON.parse(String(inOut.read())): any).data).toMatch(/[\\\/]v(?!42[\\\/]?$)[0-9]+[\\\/]?$/);

await mockConstants(config, {CACHE_VERSION: 42}, async config => {
await cache(config, reporter, {}, ['dir']);
Expand Down
5 changes: 5 additions & 0 deletions __tests__/fixtures/install/mtime-same/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"dependencies": {
"pkg": "file:./pkg-a.tgz"
}
}
Binary file added __tests__/fixtures/install/mtime-same/pkg-a.tgz
Binary file not shown.
Binary file added __tests__/fixtures/install/mtime-same/pkg-b.tgz
Binary file not shown.
2 changes: 1 addition & 1 deletion src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const YARN_INSTALLER_MSI = 'https://yarnpkg.com/latest.msi';
export const SELF_UPDATE_VERSION_URL = 'https://yarnpkg.com/latest-version';

// cache version, bump whenever we make backwards incompatible changes
export const CACHE_VERSION = 1;
export const CACHE_VERSION = 2;

// lockfile version, bump whenever we make backwards incompatible changes
export const LOCKFILE_VERSION = 1;
Expand Down
36 changes: 36 additions & 0 deletions src/fetchers/tarball-fetcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,48 @@ export default class TarballFetcher extends BaseFetcher {
} {
const integrityInfo = this._supportedIntegrity();

const now = new Date();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this closer to where it is used?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it's here because I don't want each file of a single package to have a different timestamp (like it would be the case if the Date constructor was called inside the closure where it is used).


const fs = require('fs');
const patchedFs = Object.assign({}, fs, {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh, why are we patching fs where we already have our own fs functions module?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it's not the same. The tar-fs package is doing utimes call in order to set the files timestamps. This function is broken and doesn't support subsecond timestamps. The only way to solve this without forking tar-fs is to give it an api-compatible version of the fs module, patched in order to use futimes instead of utimes.

Ideally the fix should be done upstream, but this issue is problematic enough that it warrants an hotfix to land faster.

Copy link
Member

@Daniel15 Daniel15 Aug 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to monkey patch tar-fs rather than fs? That seems like a better idea to me.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, utimes is used within a closure. I also prefer to patch a function with clearly defined (if a bit buggy) inputs/output rather than a random function from a module that can change without notice.

Also note that this code doesn't modify the global fs implementation. It simply creates a new, local, fs instance and pass it to tar-fs using its custom filesystem option, which seems perfect for this use case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree this is a good solution in regards to tar-fs. It does kind of bloat the file a little. Do you think it'll be possible to move untar-stream outside somehow?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm maybe it could be moved in src/utils/fs, yeah 👍

utimes: (path, atime, mtime, cb) => {
fs.stat(path, (err, stat) => {
if (err) {
cb(err);
return;
}
if (stat.isDirectory()) {
fs.utimes(path, atime, mtime, cb);
return;
}
fs.open(path, 'a', (err, fd) => {
if (err) {
cb(err);
return;
}
fs.futimes(fd, atime, mtime, err => {
if (err) {
fs.close(fd, () => cb(err));
} else {
fs.close(fd, err => cb(err));
}
});
});
});
},
});

const validateStream = new ssri.integrityStream(integrityInfo);
const untarStream = tarFs.extract(this.dest, {
strip: 1,
dmode: 0o755, // all dirs should be readable
fmode: 0o644, // all files should be readable
chown: false, // don't chown. just leave as it is
map: header => {
header.mtime = now;
return header;
},
fs: patchedFs,
});
const extractorStream = gunzip();

Expand Down
2 changes: 1 addition & 1 deletion src/reporters/lang/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const messages = {
verboseFileCopy: 'Copying $0 to $1.',
verboseFileLink: 'Creating hardlink at $0 to $1.',
verboseFileSymlink: 'Creating symlink at $0 to $1.',
verboseFileSkip: 'Skipping copying of file $0 as the file at $1 is the same size ($2) and has the same content.',
verboseFileSkip: 'Skipping copying of file $0 as the file at $1 is the same size ($2) and mtime ($3).',
verboseFileSkipSymlink: 'Skipping copying of $0 as the file at $1 is the same symlink ($2).',
verboseFileSkipHardlink: 'Skipping copying of $0 as the file at $1 is the same hardlink ($2).',
verboseFileRemoveExtraneous: 'Removing extraneous file $0.',
Expand Down
6 changes: 3 additions & 3 deletions src/util/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import BlockingQueue from './blocking-queue.js';
import * as promise from './promise.js';
import {promisify} from './promise.js';
import map from './map.js';
import {copyFile, unlink} from './fs-normalized.js';
import {copyFile, fileDatesEqual, unlink} from './fs-normalized.js';

export const constants =
typeof fs.constants !== 'undefined'
Expand Down Expand Up @@ -222,10 +222,10 @@ async function buildActionsForCopy(
return;
}

if (bothFiles && srcStat.size === destStat.size && fs.readFileSync(src).equals(fs.readFileSync(dest))) {
if (bothFiles && srcStat.size === destStat.size && fileDatesEqual(srcStat.mtime, destStat.mtime)) {
// we can safely assume this is the same file
onDone();
reporter.verbose(reporter.lang('verboseFileSkip', src, dest, srcStat.size));
reporter.verbose(reporter.lang('verboseFileSkip', src, dest, srcStat.size, +srcStat.mtime));
return;
}

Expand Down