mirror of
https://github.com/atom/atom.git
synced 2026-04-06 03:02:13 -04:00
Use a tree-backed registry to deduplicate and consolidate native watchers
This commit is contained in:
113
spec/native-watcher-registry-spec.js
Normal file
113
spec/native-watcher-registry-spec.js
Normal file
@@ -0,0 +1,113 @@
|
||||
/** @babel */
|
||||
|
||||
import path from 'path'
|
||||
|
||||
import NativeWatcherRegistry from '../src/native-watcher-registry'
|
||||
|
||||
class MockWatcher {
|
||||
constructor () {
|
||||
this.native = null
|
||||
}
|
||||
|
||||
attachToNative (native) {
|
||||
this.native = native
|
||||
this.native.attached.push(this)
|
||||
}
|
||||
}
|
||||
|
||||
class MockNative {
|
||||
constructor (name) {
|
||||
this.name = name
|
||||
this.attached = []
|
||||
this.disposed = false
|
||||
this.stopped = false
|
||||
}
|
||||
|
||||
reattachTo (newNative) {
|
||||
for (const watcher of this.attached) {
|
||||
watcher.attachToNative(newNative)
|
||||
}
|
||||
|
||||
this.attached = []
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposed = true
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.stopped = true
|
||||
}
|
||||
}
|
||||
|
||||
describe('NativeWatcherRegistry', function () {
|
||||
let registry, watcher
|
||||
|
||||
beforeEach(function () {
|
||||
registry = new NativeWatcherRegistry()
|
||||
watcher = new MockWatcher()
|
||||
})
|
||||
|
||||
it('attaches a Watcher to a newly created NativeWatcher for a new directory', function() {
|
||||
const NATIVE = new MockNative('created')
|
||||
registry.attach('/some/path', watcher, () => NATIVE)
|
||||
|
||||
expect(watcher.native).toBe(NATIVE)
|
||||
})
|
||||
|
||||
it('reuses an existing NativeWatcher on the same directory', function () {
|
||||
const EXISTING = new MockNative('existing')
|
||||
registry.attach('/existing/path', new MockWatcher(), () => EXISTING)
|
||||
|
||||
registry.attach('/existing/path', watcher, () => new MockNative('no'))
|
||||
|
||||
expect(watcher.native).toBe(EXISTING)
|
||||
})
|
||||
|
||||
it('attaches to an existing NativeWatcher on a parent directory', function () {
|
||||
const EXISTING = new MockNative('existing')
|
||||
registry.attach('/existing/path', new MockWatcher(), () => EXISTING)
|
||||
|
||||
registry.attach('/existing/path/sub/directory/', watcher, () => new MockNative('no'))
|
||||
|
||||
expect(watcher.native).toBe(EXISTING)
|
||||
})
|
||||
|
||||
it('adopts Watchers from NativeWatchers on child directories', function () {
|
||||
const EXISTING0 = new MockNative('existing0')
|
||||
const watcher0 = new MockWatcher()
|
||||
registry.attach('/existing/path/child/directory/zero', watcher0, () => EXISTING0)
|
||||
|
||||
const EXISTING1 = new MockNative('existing1')
|
||||
const watcher1 = new MockWatcher()
|
||||
registry.attach('/existing/path/child/directory/one', watcher1, () => EXISTING1)
|
||||
|
||||
const EXISTING2 = new MockNative('existing2')
|
||||
const watcher2 = new MockWatcher()
|
||||
registry.attach('/another/path', watcher2, () => EXISTING2)
|
||||
|
||||
expect(watcher0.native).toBe(EXISTING0)
|
||||
expect(watcher1.native).toBe(EXISTING1)
|
||||
expect(watcher2.native).toBe(EXISTING2)
|
||||
|
||||
// Consolidate all three watchers beneath the same native watcher on the parent directory
|
||||
const CREATED = new MockNative('created')
|
||||
registry.attach('/existing/path/', watcher, () => CREATED)
|
||||
|
||||
expect(watcher.native).toBe(CREATED)
|
||||
|
||||
expect(watcher0.native).toBe(CREATED)
|
||||
expect(EXISTING0.stopped).toBe(true)
|
||||
expect(EXISTING0.disposed).toBe(true)
|
||||
|
||||
expect(watcher1.native).toBe(CREATED)
|
||||
expect(EXISTING1.stopped).toBe(true)
|
||||
expect(EXISTING1.disposed).toBe(true)
|
||||
|
||||
expect(watcher2.native).toBe(EXISTING2)
|
||||
expect(EXISTING2.stopped).toBe(false)
|
||||
expect(EXISTING2.disposed).toBe(false)
|
||||
})
|
||||
|
||||
it('removes NativeWatchers when all Watchers have been disposed')
|
||||
})
|
||||
240
src/native-watcher-registry.js
Normal file
240
src/native-watcher-registry.js
Normal file
@@ -0,0 +1,240 @@
|
||||
/** @babel */
|
||||
|
||||
import path from 'path'
|
||||
|
||||
// Private: Non-leaf node in a tree used by the {NativeWatcherRegistry} to cover the allocated {Watcher} instances with
|
||||
// the most efficient set of {NativeWatcher} instances possible. Each {RegistryNode} maps to a directory in the
|
||||
// filesystem tree.
|
||||
class RegistryNode {
|
||||
|
||||
// Private: Construct a new, empty node representing a node with no watchers.
|
||||
constructor () {
|
||||
this.children = {}
|
||||
}
|
||||
|
||||
// Private: Recursively discover any existing watchers corresponding to a path.
|
||||
//
|
||||
// * `pathSegments` filesystem path of a new {Watcher} already split into an Array of directory names.
|
||||
//
|
||||
// Returns: A {ParentResult} if the exact requested directory or a parent directory is being watched, a
|
||||
// {ChildrenResult} if one or more child paths are being watched, or a {MissingResult} if no relevant watchers
|
||||
// exist.
|
||||
lookup(pathSegments) {
|
||||
if (pathSegments.length === 0) {
|
||||
return new ChildrenResult(this.leaves())
|
||||
}
|
||||
|
||||
const child = this.children[pathSegments[0]]
|
||||
if (child === undefined) {
|
||||
return new MissingResult(this)
|
||||
}
|
||||
|
||||
return child.lookup(pathSegments.slice(1))
|
||||
}
|
||||
|
||||
// Private: Insert a new {RegistryWatcherNode} into the tree, creating new intermediate {RegistryNode} instances as
|
||||
// needed. Any existing children of the watched directory are removed.
|
||||
//
|
||||
// * `pathSegments` filesystem path of the new {Watcher}, already split into an Array of directory names.
|
||||
// * `leaf` initialized {RegistryWatcherNode} to insert
|
||||
//
|
||||
// Returns: The root of a new tree with the {RegistryWatcherNode} inserted at the correct location. Callers should
|
||||
// replace their node references with the returned value.
|
||||
insert(pathSegments, leaf) {
|
||||
if (pathSegments.length === 0) {
|
||||
return leaf
|
||||
}
|
||||
|
||||
const pathKey = pathSegments[0]
|
||||
let child = this.children[pathKey]
|
||||
if (child === undefined) {
|
||||
child = new RegistryNode()
|
||||
}
|
||||
this.children[pathKey] = child.insert(pathSegments.slice(1), leaf)
|
||||
return this
|
||||
}
|
||||
|
||||
// Private: Discover all {RegistryWatcherNode} instances beneath this tree node.
|
||||
//
|
||||
// Returns: A possibly empty {Array} of {RegistryWatcherNode} instances that are the descendants of this node.
|
||||
leaves() {
|
||||
const results = []
|
||||
for (const p of Object.keys(this.children)) {
|
||||
results.push(...this.children[p].leaves())
|
||||
}
|
||||
return results
|
||||
}
|
||||
}
|
||||
|
||||
// Private: Leaf node within a {NativeWatcherRegistry} tree. Represents a directory that is covered by a
|
||||
// {NativeWatcher}.
|
||||
class RegistryWatcherNode {
|
||||
|
||||
// Private: Allocate a new node to track a {NativeWatcher}.
|
||||
//
|
||||
// * `nativeWatcher` An existing {NativeWatcher} instance.
|
||||
constructor (nativeWatcher) {
|
||||
this.nativeWatcher = nativeWatcher
|
||||
}
|
||||
|
||||
// Private: Accessor for the {NativeWatcher}.
|
||||
getNativeWatcher () {
|
||||
return this.nativeWatcher
|
||||
}
|
||||
|
||||
// Private: Identify how this watcher relates to a request to watch a directory tree.
|
||||
//
|
||||
// * `pathSegments` filesystem path of a new {Watcher} already split into an Array of directory names.
|
||||
//
|
||||
// Returns: A {ParentResult} referencing this node.
|
||||
lookup(pathSegments) {
|
||||
return new ParentResult(this, pathSegments)
|
||||
}
|
||||
|
||||
// Private: Discover this {RegistryWatcherNode} instance.
|
||||
//
|
||||
// Returns: An {Array} containing this node.
|
||||
leaves() {
|
||||
return [this]
|
||||
}
|
||||
}
|
||||
|
||||
// Private: A {RegisteryNode} traversal result that's returned when neither a directory, its children, nor its parents
|
||||
// are present in the tree.
|
||||
class MissingResult {
|
||||
// Private: Instantiate a new {MissingResult}.
|
||||
//
|
||||
// * `lastParent` the final succesfully traversed {RegistryNode}.
|
||||
constructor (lastParent) {
|
||||
this.lastParent = lastParent
|
||||
}
|
||||
|
||||
// Private: Dispatch within a map of callback actions.
|
||||
//
|
||||
// * `actions` {Object} containing a `missing` key that maps to a callback to be invoked when no results were returned
|
||||
// by {RegistryNode.lookup}. The callback will be called with the last parent node that was encountered during the
|
||||
// traversal.
|
||||
//
|
||||
// Returns: the result of the `actions` callback.
|
||||
when (actions) {
|
||||
return actions.missing(this.lastParent)
|
||||
}
|
||||
}
|
||||
|
||||
// Private: A {RegistryNode.lookup} traversal result that's returned when a parent or an exact match of the requested
|
||||
// directory is being watched by an existing {RegistryWatcherNode}.
|
||||
class ParentResult {
|
||||
|
||||
// Private: Instantiate a new {ParentResult}.
|
||||
//
|
||||
// * `parent` the {RegistryWatcherNode} that was discovered.
|
||||
// * `remainingPathSegments` an {Array} of the directories that lie between the leaf node's watched directory and
|
||||
// the requested directory. This will be empty for exact matches.
|
||||
constructor (parent, remainingPathSegments) {
|
||||
this.parent = parent
|
||||
this.remainingPathSegments = remainingPathSegments
|
||||
}
|
||||
|
||||
// Private: Dispatch within a map of callback actions.
|
||||
//
|
||||
// * `actions` {Object} containing a `parent` key that maps to a callback to be invoked when a parent of a requested
|
||||
// requested directory is returned by a {RegistryNode.lookup} call. The callback will be called with the
|
||||
// {RegistryWatcherNode} instance and an {Array} of the {String} path segments that separate the parent node
|
||||
// and the requested directory.
|
||||
//
|
||||
// Returns: the result of the `actions` callback.
|
||||
when (actions) {
|
||||
return actions.parent(this.parent, this.remainingPathSegments)
|
||||
}
|
||||
}
|
||||
|
||||
// Private: A {RegistryNode.lookup} traversal result that's returned when one or more children of the requested
|
||||
// directory are already being watched.
|
||||
class ChildrenResult {
|
||||
|
||||
// Private: Instantiate a new {ChildrenResult}.
|
||||
//
|
||||
// * `children` {Array} of the {RegistryWatcherNode} instances that were discovered.
|
||||
constructor (children) {
|
||||
this.children = children
|
||||
}
|
||||
|
||||
// Private: Dispatch within a map of callback actions.
|
||||
//
|
||||
// * `actions` {Object} containing a `children` key that maps to a callback to be invoked when a parent of a requested
|
||||
// requested directory is returned by a {RegistryNode.lookup} call. The callback will be called with the
|
||||
// {RegistryWatcherNode} instance.
|
||||
//
|
||||
// Returns: the result of the `actions` callback.
|
||||
when (actions) {
|
||||
return actions.children(this.children)
|
||||
}
|
||||
}
|
||||
|
||||
// Private: Track the directories being monitored by native filesystem watchers. Minimize the number of native watchers
|
||||
// allocated to receive events for a desired set of directories by:
|
||||
//
|
||||
// 1. Subscribing to the same underlying {NativeWatcher} when watching the same directory multiple times.
|
||||
// 2. Subscribing to an existing {NativeWatcher} on a parent of a desired directory.
|
||||
// 3. Replacing multiple {NativeWatcher} instances on child directories with a single new {NativeWatcher} on the
|
||||
// parent.
|
||||
export default class NativeWatcherRegistry {
|
||||
|
||||
// Private: Instantiate an empty registry.
|
||||
constructor () {
|
||||
this.tree = new RegistryNode()
|
||||
}
|
||||
|
||||
// Private: Attach a watcher to a directory, assigning it a {NativeWatcher}. If a suitable {NativeWatcher} already
|
||||
// exists, it will be attached to the new {Watcher} with an appropriate subpath configuration. Otherwise, the
|
||||
// `createWatcher` callback will be invoked to create a new {NativeWatcher}, which will be registered in the tree
|
||||
// and attached to the watcher.
|
||||
//
|
||||
// If any pre-existing child watchers are removed as a result of this operation, {NativeWatcher.onWillReattach} will
|
||||
// be broadcast on each with the new parent watcher as an event payload to give child watchers a chance to attach to
|
||||
// the new watcher.
|
||||
//
|
||||
// * `directory` a normalized path to be watched as a {String}.
|
||||
// * `watcher` an unattached {Watcher}.
|
||||
// * `createNative` callback to be invoked if no existing {NativeWatcher} covers the {Watcher}. It should
|
||||
// synchronously return a new {NativeWatcher} instance watching {directory}.
|
||||
attach (directory, watcher, createNative) {
|
||||
const pathSegments = directory.split(path.sep).filter(segment => segment.length > 0)
|
||||
|
||||
const attachToNew = () => {
|
||||
const native = createNative()
|
||||
const leaf = new RegistryWatcherNode(native)
|
||||
this.tree = this.tree.insert(pathSegments, leaf)
|
||||
|
||||
watcher.attachToNative(native, '')
|
||||
|
||||
return native
|
||||
}
|
||||
|
||||
this.tree.lookup(pathSegments).when({
|
||||
parent: (parent, remaining) => {
|
||||
// An existing NativeWatcher is watching a parent directory of the requested path. Attach this Watcher to
|
||||
// it as a filtering watcher.
|
||||
const native = parent.getNativeWatcher()
|
||||
const subpath = remaining.length === 0 ? '' : path.join(...remaining)
|
||||
|
||||
watcher.attachToNative(native, subpath)
|
||||
},
|
||||
children: children => {
|
||||
const newNative = attachToNew()
|
||||
|
||||
// One or more NativeWatchers exist on child directories of the requested path.
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = children[i]
|
||||
const childNative = child.getNativeWatcher()
|
||||
childNative.reattachTo(newNative, directory)
|
||||
childNative.dispose()
|
||||
|
||||
// Don't await this Promise. Subscribers can listen for `onDidStop` to be notified if they choose.
|
||||
childNative.stop()
|
||||
}
|
||||
},
|
||||
missing: attachToNew
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user