fs: add *timeNs properties to BigInt Stats objects

- Extend the aliased buffer for stats objects to contain
  the entire time spec (seconds and nanoseconds) for the time
  values instead of calculating the milliseconds in C++ and
  lose precision there.
- Calculate the nanosecond-precision time values in JS and expose
  them in BigInt Stats objects as `*timeNs`. The
  millisecond-precision values are now calculated from the
  nanosecond-precision values.

PR-URL: https://github.com/nodejs/node/pull/21387
Reviewed-By: Jeremiah Senkpiel <fishrock123@rocketmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gus Caplan <me@gus.host>
This commit is contained in:
Joyee Cheung 2018-06-19 02:58:49 +08:00
parent 4b1bcae681
commit b245257b70
No known key found for this signature in database
GPG Key ID: 92B78A53C8303B8D
7 changed files with 288 additions and 142 deletions

View File

@ -512,7 +512,8 @@ A `fs.Stats` object provides information about a file.
Objects returned from [`fs.stat()`][], [`fs.lstat()`][] and [`fs.fstat()`][] and
their synchronous counterparts are of this type.
If `bigint` in the `options` passed to those methods is true, the numeric values
will be `bigint` instead of `number`.
will be `bigint` instead of `number`, and the object will contain additional
nanosecond-precision properties suffixed with `Ns`.
```console
Stats {
@ -539,7 +540,7 @@ Stats {
`bigint` version:
```console
Stats {
BigIntStats {
dev: 2114n,
ino: 48064969n,
mode: 33188n,
@ -554,6 +555,10 @@ Stats {
mtimeMs: 1318289051000n,
ctimeMs: 1318289051000n,
birthtimeMs: 1318289051000n,
atimeNs: 1318289051000000000n,
mtimeNs: 1318289051000000000n,
ctimeNs: 1318289051000000000n,
birthtimeNs: 1318289051000000000n,
atime: Mon, 10 Oct 2011 23:24:11 GMT,
mtime: Mon, 10 Oct 2011 23:24:11 GMT,
ctime: Mon, 10 Oct 2011 23:24:11 GMT,
@ -726,6 +731,54 @@ added: v8.1.0
The timestamp indicating the creation time of this file expressed in
milliseconds since the POSIX Epoch.
### stats.atimeNs
<!-- YAML
added: REPLACEME
-->
* {bigint}
Only present when `bigint: true` is passed into the method that generates
the object.
The timestamp indicating the last time this file was accessed expressed in
nanoseconds since the POSIX Epoch.
### stats.mtimeNs
<!-- YAML
added: REPLACEME
-->
* {bigint}
Only present when `bigint: true` is passed into the method that generates
the object.
The timestamp indicating the last time this file was modified expressed in
nanoseconds since the POSIX Epoch.
### stats.ctimeNs
<!-- YAML
added: REPLACEME
-->
* {bigint}
Only present when `bigint: true` is passed into the method that generates
the object.
The timestamp indicating the last time the file status was changed expressed
in nanoseconds since the POSIX Epoch.
### stats.birthtimeNs
<!-- YAML
added: REPLACEME
-->
* {bigint}
Only present when `bigint: true` is passed into the method that generates
the object.
The timestamp indicating the creation time of this file expressed in
nanoseconds since the POSIX Epoch.
### stats.atime
<!-- YAML
added: v0.11.13
@ -765,8 +818,17 @@ The timestamp indicating the creation time of this file.
### Stat Time Values
The `atimeMs`, `mtimeMs`, `ctimeMs`, `birthtimeMs` properties are
[numbers][MDN-Number] that hold the corresponding times in milliseconds. Their
precision is platform specific. `atime`, `mtime`, `ctime`, and `birthtime` are
numeric values that hold the corresponding times in milliseconds. Their
precision is platform specific. When `bigint: true` is passed into the
method that generates the object, the properties will be [bigints][],
otherwise they will be [numbers][MDN-Number].
The `atimeNs`, `mtimeNs`, `ctimeNs`, `birthtimeNs` properties are
[bigints][] that hold the corresponding times in nanoseconds. They are
only present when `bigint: true` is passed into the method that generates
the object. Their precision is platform specific.
`atime`, `mtime`, `ctime`, and `birthtime` are
[`Date`][MDN-Date] object alternate representations of the various times. The
`Date` and number values are not connected. Assigning a new number value, or
mutating the `Date` value, will not be reflected in the corresponding alternate
@ -4976,6 +5038,7 @@ the file contents.
[`net.Socket`]: net.html#net_class_net_socket
[`stat()`]: fs.html#fs_fs_stat_path_options_callback
[`util.promisify()`]: util.html#util_util_promisify_original
[bigints]: https://tc39.github.io/proposal-bigint
[Caveats]: #fs_caveats
[Common System Errors]: errors.html#errors_common_system_errors
[FS Constants]: #fs_fs_constants_1

View File

@ -1,6 +1,6 @@
'use strict';
const { Reflect } = primordials;
const { Object, Reflect } = primordials;
const { Buffer, kMaxLength } = require('buffer');
const {
@ -16,7 +16,8 @@ const {
} = require('internal/errors');
const {
isUint8Array,
isDate
isDate,
isBigUint64Array
} = require('internal/util/types');
const { once } = require('internal/util');
const { toPathIfFileURL } = require('internal/url');
@ -230,27 +231,9 @@ function preprocessSymlinkDestination(path, type, linkPath) {
}
}
function dateFromNumeric(num) {
return new Date(Number(num) + 0.5);
}
// Constructor for file stats.
function Stats(
dev,
mode,
nlink,
uid,
gid,
rdev,
blksize,
ino,
size,
blocks,
atim_msec,
mtim_msec,
ctim_msec,
birthtim_msec
) {
function StatsBase(dev, mode, nlink, uid, gid, rdev, blksize,
ino, size, blocks) {
this.dev = dev;
this.mode = mode;
this.nlink = nlink;
@ -261,63 +244,132 @@ function Stats(
this.ino = ino;
this.size = size;
this.blocks = blocks;
this.atimeMs = atim_msec;
this.mtimeMs = mtim_msec;
this.ctimeMs = ctim_msec;
this.birthtimeMs = birthtim_msec;
this.atime = dateFromNumeric(atim_msec);
this.mtime = dateFromNumeric(mtim_msec);
this.ctime = dateFromNumeric(ctim_msec);
this.birthtime = dateFromNumeric(birthtim_msec);
}
StatsBase.prototype.isDirectory = function() {
return this._checkModeProperty(S_IFDIR);
};
StatsBase.prototype.isFile = function() {
return this._checkModeProperty(S_IFREG);
};
StatsBase.prototype.isBlockDevice = function() {
return this._checkModeProperty(S_IFBLK);
};
StatsBase.prototype.isCharacterDevice = function() {
return this._checkModeProperty(S_IFCHR);
};
StatsBase.prototype.isSymbolicLink = function() {
return this._checkModeProperty(S_IFLNK);
};
StatsBase.prototype.isFIFO = function() {
return this._checkModeProperty(S_IFIFO);
};
StatsBase.prototype.isSocket = function() {
return this._checkModeProperty(S_IFSOCK);
};
const kNsPerMsBigInt = 10n ** 6n;
const kNsPerSecBigInt = 10n ** 9n;
const kMsPerSec = 10 ** 3;
const kNsPerMs = 10 ** 6;
function msFromTimeSpec(sec, nsec) {
return sec * kMsPerSec + nsec / kNsPerMs;
}
function nsFromTimeSpecBigInt(sec, nsec) {
return sec * kNsPerSecBigInt + nsec;
}
function dateFromMs(ms) {
return new Date(Number(ms) + 0.5);
}
function BigIntStats(dev, mode, nlink, uid, gid, rdev, blksize,
ino, size, blocks,
atimeNs, mtimeNs, ctimeNs, birthtimeNs) {
StatsBase.call(this, dev, mode, nlink, uid, gid, rdev, blksize,
ino, size, blocks);
this.atimeMs = atimeNs / kNsPerMsBigInt;
this.mtimeMs = mtimeNs / kNsPerMsBigInt;
this.ctimeMs = ctimeNs / kNsPerMsBigInt;
this.birthtimeMs = birthtimeNs / kNsPerMsBigInt;
this.atimeNs = atimeNs;
this.mtimeNs = mtimeNs;
this.ctimeNs = ctimeNs;
this.birthtimeNs = birthtimeNs;
this.atime = dateFromMs(this.atimeMs);
this.mtime = dateFromMs(this.mtimeMs);
this.ctime = dateFromMs(this.ctimeMs);
this.birthtime = dateFromMs(this.birthtimeMs);
}
Object.setPrototypeOf(BigIntStats.prototype, StatsBase.prototype);
Object.setPrototypeOf(BigIntStats, StatsBase);
BigIntStats.prototype._checkModeProperty = function(property) {
if (isWindows && (property === S_IFIFO || property === S_IFBLK ||
property === S_IFSOCK)) {
return false; // Some types are not available on Windows
}
return (this.mode & BigInt(S_IFMT)) === BigInt(property);
};
function Stats(dev, mode, nlink, uid, gid, rdev, blksize,
ino, size, blocks,
atimeMs, mtimeMs, ctimeMs, birthtimeMs) {
StatsBase.call(this, dev, mode, nlink, uid, gid, rdev, blksize,
ino, size, blocks);
this.atimeMs = atimeMs;
this.mtimeMs = mtimeMs;
this.ctimeMs = ctimeMs;
this.birthtimeMs = birthtimeMs;
this.atime = dateFromMs(atimeMs);
this.mtime = dateFromMs(mtimeMs);
this.ctime = dateFromMs(ctimeMs);
this.birthtime = dateFromMs(birthtimeMs);
}
Object.setPrototypeOf(Stats.prototype, StatsBase.prototype);
Object.setPrototypeOf(Stats, StatsBase);
Stats.prototype._checkModeProperty = function(property) {
if (isWindows && (property === S_IFIFO || property === S_IFBLK ||
property === S_IFSOCK)) {
return false; // Some types are not available on Windows
}
if (typeof this.mode === 'bigint') { // eslint-disable-line valid-typeof
return (this.mode & BigInt(S_IFMT)) === BigInt(property);
}
return (this.mode & S_IFMT) === property;
};
Stats.prototype.isDirectory = function() {
return this._checkModeProperty(S_IFDIR);
};
Stats.prototype.isFile = function() {
return this._checkModeProperty(S_IFREG);
};
Stats.prototype.isBlockDevice = function() {
return this._checkModeProperty(S_IFBLK);
};
Stats.prototype.isCharacterDevice = function() {
return this._checkModeProperty(S_IFCHR);
};
Stats.prototype.isSymbolicLink = function() {
return this._checkModeProperty(S_IFLNK);
};
Stats.prototype.isFIFO = function() {
return this._checkModeProperty(S_IFIFO);
};
Stats.prototype.isSocket = function() {
return this._checkModeProperty(S_IFSOCK);
};
function getStatsFromBinding(stats, offset = 0) {
return new Stats(stats[0 + offset], stats[1 + offset], stats[2 + offset],
stats[3 + offset], stats[4 + offset], stats[5 + offset],
stats[6 + offset], // blksize
stats[7 + offset], stats[8 + offset],
stats[9 + offset], // blocks
stats[10 + offset], stats[11 + offset],
stats[12 + offset], stats[13 + offset]);
if (isBigUint64Array(stats)) {
return new BigIntStats(
stats[0 + offset], stats[1 + offset], stats[2 + offset],
stats[3 + offset], stats[4 + offset], stats[5 + offset],
stats[6 + offset], stats[7 + offset], stats[8 + offset],
stats[9 + offset],
nsFromTimeSpecBigInt(stats[10 + offset], stats[11 + offset]),
nsFromTimeSpecBigInt(stats[12 + offset], stats[13 + offset]),
nsFromTimeSpecBigInt(stats[14 + offset], stats[15 + offset]),
nsFromTimeSpecBigInt(stats[16 + offset], stats[17 + offset])
);
}
return new Stats(
stats[0 + offset], stats[1 + offset], stats[2 + offset],
stats[3 + offset], stats[4 + offset], stats[5 + offset],
stats[6 + offset], stats[7 + offset], stats[8 + offset],
stats[9 + offset],
msFromTimeSpec(stats[10 + offset], stats[11 + offset]),
msFromTimeSpec(stats[12 + offset], stats[13 + offset]),
msFromTimeSpec(stats[14 + offset], stats[15 + offset]),
msFromTimeSpec(stats[16 + offset], stats[17 + offset])
);
}
function stringToFlags(flags) {
@ -453,6 +505,7 @@ function warnOnNonPortableTemplate(template) {
module.exports = {
assertEncoding,
BigIntStats, // for testing
copyObject,
Dirent,
getDirents,

View File

@ -102,10 +102,32 @@ struct PackageConfig {
};
} // namespace loader
enum class FsStatsOffset {
kDev = 0,
kMode,
kNlink,
kUid,
kGid,
kRdev,
kBlkSize,
kIno,
kSize,
kBlocks,
kATimeSec,
kATimeNsec,
kMTimeSec,
kMTimeNsec,
kCTimeSec,
kCTimeNsec,
kBirthTimeSec,
kBirthTimeNsec,
kFsStatsFieldsNumber
};
// Stat fields buffers contain twice the number of entries in an uv_stat_t
// because `fs.StatWatcher` needs room to store 2 `fs.Stats` instances.
constexpr size_t kFsStatsFieldsNumber = 14;
constexpr size_t kFsStatsBufferLength = kFsStatsFieldsNumber * 2;
constexpr size_t kFsStatsBufferLength =
static_cast<size_t>(FsStatsOffset::kFsStatsFieldsNumber) * 2;
// PER_ISOLATE_* macros: We have a lot of per-isolate properties
// and adding and maintaining their getters and setters by hand would be

View File

@ -2197,10 +2197,13 @@ void Initialize(Local<Object> target,
env->SetMethod(target, "mkdtemp", Mkdtemp);
target->Set(context,
FIXED_ONE_BYTE_STRING(isolate, "kFsStatsFieldsNumber"),
Integer::New(isolate, kFsStatsFieldsNumber))
.Check();
target
->Set(context,
FIXED_ONE_BYTE_STRING(isolate, "kFsStatsFieldsNumber"),
Integer::New(
isolate,
static_cast<int32_t>(FsStatsOffset::kFsStatsFieldsNumber)))
.Check();
target->Set(context,
FIXED_ONE_BYTE_STRING(isolate, "statValues"),

View File

@ -6,6 +6,7 @@
#include "node.h"
#include "stream_base.h"
#include "req_wrap-inl.h"
#include <iostream>
namespace node {
@ -149,74 +150,48 @@ class FSReqCallback : public FSReqBase {
FSReqCallback& operator=(const FSReqCallback&) = delete;
};
// Wordaround a GCC4.9 bug that C++14 N3652 was not implemented
// Refs: https://www.gnu.org/software/gcc/projects/cxx-status.html#cxx14
// Refs: https://isocpp.org/files/papers/N3652.html
#if __cpp_constexpr < 201304
# define constexpr inline
#endif
template <typename NativeT,
// SFINAE limit NativeT to arithmetic types
typename = std::enable_if<std::is_arithmetic<NativeT>::value>>
constexpr NativeT ToNative(uv_timespec_t ts) {
// This template has exactly two specializations below.
static_assert(std::is_arithmetic<NativeT>::value == false, "Not implemented");
return NativeT();
}
template <>
constexpr double ToNative(uv_timespec_t ts) {
// We need to do a static_cast since the original FS values are ulong.
/* NOLINTNEXTLINE(runtime/int) */
const auto u_sec = static_cast<unsigned long>(ts.tv_sec);
const double full_sec = u_sec * 1000.0;
/* NOLINTNEXTLINE(runtime/int) */
const auto u_nsec = static_cast<unsigned long>(ts.tv_nsec);
const double full_nsec = u_nsec / 1000'000.0;
return full_sec + full_nsec;
}
template <>
constexpr uint64_t ToNative(uv_timespec_t ts) {
// We need to do a static_cast since the original FS values are ulong.
/* NOLINTNEXTLINE(runtime/int) */
const auto u_sec = static_cast<unsigned long>(ts.tv_sec);
const auto full_sec = static_cast<uint64_t>(u_sec) * 1000UL;
/* NOLINTNEXTLINE(runtime/int) */
const auto u_nsec = static_cast<unsigned long>(ts.tv_nsec);
const auto full_nsec = static_cast<uint64_t>(u_nsec) / 1000'000UL;
return full_sec + full_nsec;
}
#undef constexpr // end N3652 bug workaround
template <typename NativeT, typename V8T>
constexpr void FillStatsArray(AliasedBufferBase<NativeT, V8T>* fields,
const uv_stat_t* s,
const size_t offset = 0) {
fields->SetValue(offset + 0, static_cast<NativeT>(s->st_dev));
fields->SetValue(offset + 1, static_cast<NativeT>(s->st_mode));
fields->SetValue(offset + 2, static_cast<NativeT>(s->st_nlink));
fields->SetValue(offset + 3, static_cast<NativeT>(s->st_uid));
fields->SetValue(offset + 4, static_cast<NativeT>(s->st_gid));
fields->SetValue(offset + 5, static_cast<NativeT>(s->st_rdev));
fields->SetValue(offset + 6, static_cast<NativeT>(s->st_blksize));
fields->SetValue(offset + 7, static_cast<NativeT>(s->st_ino));
fields->SetValue(offset + 8, static_cast<NativeT>(s->st_size));
fields->SetValue(offset + 9, static_cast<NativeT>(s->st_blocks));
// Dates.
fields->SetValue(offset + 10, ToNative<NativeT>(s->st_atim));
fields->SetValue(offset + 11, ToNative<NativeT>(s->st_mtim));
fields->SetValue(offset + 12, ToNative<NativeT>(s->st_ctim));
fields->SetValue(offset + 13, ToNative<NativeT>(s->st_birthtim));
#define SET_FIELD_WITH_STAT(stat_offset, stat) \
fields->SetValue(offset + static_cast<size_t>(FsStatsOffset::stat_offset), \
static_cast<NativeT>(stat))
#define SET_FIELD_WITH_TIME_STAT(stat_offset, stat) \
/* NOLINTNEXTLINE(runtime/int) */ \
SET_FIELD_WITH_STAT(stat_offset, static_cast<unsigned long>(stat))
SET_FIELD_WITH_STAT(kDev, s->st_dev);
SET_FIELD_WITH_STAT(kMode, s->st_mode);
SET_FIELD_WITH_STAT(kNlink, s->st_nlink);
SET_FIELD_WITH_STAT(kUid, s->st_uid);
SET_FIELD_WITH_STAT(kGid, s->st_gid);
SET_FIELD_WITH_STAT(kRdev, s->st_rdev);
SET_FIELD_WITH_STAT(kBlkSize, s->st_blksize);
SET_FIELD_WITH_STAT(kIno, s->st_ino);
SET_FIELD_WITH_STAT(kSize, s->st_size);
SET_FIELD_WITH_STAT(kBlocks, s->st_blocks);
SET_FIELD_WITH_TIME_STAT(kATimeSec, s->st_atim.tv_sec);
SET_FIELD_WITH_TIME_STAT(kATimeNsec, s->st_atim.tv_nsec);
SET_FIELD_WITH_TIME_STAT(kMTimeSec, s->st_mtim.tv_sec);
SET_FIELD_WITH_TIME_STAT(kMTimeNsec, s->st_mtim.tv_nsec);
SET_FIELD_WITH_TIME_STAT(kCTimeSec, s->st_ctim.tv_sec);
SET_FIELD_WITH_TIME_STAT(kCTimeNsec, s->st_ctim.tv_nsec);
SET_FIELD_WITH_TIME_STAT(kBirthTimeSec, s->st_birthtim.tv_sec);
SET_FIELD_WITH_TIME_STAT(kBirthTimeNsec, s->st_birthtim.tv_nsec);
#undef SET_FIELD_WITH_TIME_STAT
#undef SET_FIELD_WITH_STAT
}
inline Local<Value> FillGlobalStatsArray(Environment* env,
const bool use_bigint,
const uv_stat_t* s,
const bool second = false) {
const ptrdiff_t offset = second ? kFsStatsFieldsNumber : 0;
const ptrdiff_t offset =
second ? static_cast<ptrdiff_t>(FsStatsOffset::kFsStatsFieldsNumber) : 0;
if (use_bigint) {
auto* const arr = env->fs_stats_field_bigint_array();
FillStatsArray(arr, s, offset);
@ -302,7 +277,9 @@ class FSReqPromise : public FSReqBase {
private:
FSReqPromise(Environment* env, v8::Local<v8::Object> obj, bool use_bigint)
: FSReqBase(env, obj, AsyncWrap::PROVIDER_FSREQPROMISE, use_bigint),
stats_field_array_(env->isolate(), kFsStatsFieldsNumber) {}
stats_field_array_(
env->isolate(),
static_cast<size_t>(FsStatsOffset::kFsStatsFieldsNumber)) {}
bool finished_ = false;
AliasedBufferT stats_field_array_;

View File

@ -59,6 +59,26 @@ function verifyStats(bigintStats, numStats) {
bigintStats.isSymbolicLink(),
numStats.isSymbolicLink()
);
} else if (key.endsWith('Ms')) {
const nsKey = key.replace('Ms', 'Ns');
const msFromBigInt = bigintStats[key];
const nsFromBigInt = bigintStats[nsKey];
const msFromBigIntNs = Number(nsFromBigInt / (10n ** 6n));
const msFromNum = numStats[key];
// The difference between the millisecond-precision values should be
// smaller than 2
assert(
Math.abs(msFromNum - Number(msFromBigInt)) < 2,
`Number version ${key} = ${msFromNum}, ` +
`BigInt version ${key} = ${msFromBigInt}n`);
// The difference between the millisecond-precision value and the
// nanosecond-precision value scaled down to milliseconds should be
// smaller than 2
assert(
Math.abs(msFromNum - Number(msFromBigIntNs)) < 2,
`Number version ${key} = ${msFromNum}, ` +
`BigInt version ${nsKey} = ${nsFromBigInt}n` +
` = ${msFromBigIntNs}ms`);
} else if (Number.isSafeInteger(val)) {
assert.strictEqual(
bigintStats[key], BigInt(val),

View File

@ -1,14 +1,18 @@
'use strict';
// Flags: --expose-internals
const common = require('../common');
const assert = require('assert');
const { BigIntStats } = require('internal/fs/utils');
const fs = require('fs');
const path = require('path');
const tmpdir = require('../common/tmpdir');
const enoentFile = path.join(tmpdir.path, 'non-existent-file');
const expectedStatObject = new fs.Stats(
const expectedStatObject = new BigIntStats(
0n, // dev
0n, // mode
0n, // nlink
@ -19,10 +23,14 @@ const expectedStatObject = new fs.Stats(
0n, // ino
0n, // size
0n, // blocks
0n, // atim_msec
0n, // mtim_msec
0n, // ctim_msec
0n // birthtim_msec
0n, // atimeMs
0n, // mtimeMs
0n, // ctimeMs
0n, // birthtimeMs
0n, // atimeNs
0n, // mtimeNs
0n, // ctimeNs
0n // birthtimeNs
);
tmpdir.refresh();