-
Notifications
You must be signed in to change notification settings - Fork 295
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
Performance optimizations #1764
Changes from all commits
4231e81
2b58b6a
024416a
e003226
5c2c418
abeb197
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { Injectable } from '@angular/core'; | ||
import 'intra-dependent'; | ||
|
||
@Injectable() | ||
export class PrimaryAngularService { | ||
initialize() { | ||
// stub | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
/** | ||
* Registers the absolute specifiers of libraries that have been processed by ngcc. This cache is | ||
* reused across all entry-points of a package, so module requests across the entry-points can | ||
* determine whether invoking ngcc is necessary. | ||
* | ||
* The cost of invoking ngcc for an entry-point that has already been processed is limited due to | ||
* a fast path in ngcc, however even in this fast-path does ngcc scan the entry-point to determine | ||
* if all dependencies have been processed. This cache allows to avoid that work, as entry-points | ||
* are processed in batches during which the `node_modules` directory is not mutated. | ||
*/ | ||
export class NgccProcessingCache { | ||
private readonly processedModuleNames = new Set<string>(); | ||
|
||
hasProcessed(moduleName: string): boolean { | ||
return this.processedModuleNames.has(moduleName); | ||
} | ||
|
||
markProcessed(moduleName: string): void { | ||
this.processedModuleNames.add(moduleName); | ||
} | ||
|
||
clear(): void { | ||
this.processedModuleNames.clear(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -101,7 +101,12 @@ export const packageTransformFactory = ( | |
const ngPkg: PackageNode = foundNode; | ||
const entryPoints = [ngPkg.data.primary, ...ngPkg.data.secondaries].map(entryPoint => { | ||
const { destinationFiles, moduleId } = entryPoint; | ||
const node = new EntryPointNode(ngUrl(moduleId), ngPkg.cache.sourcesFileCache); | ||
const node = new EntryPointNode( | ||
ngUrl(moduleId), | ||
ngPkg.cache.sourcesFileCache, | ||
ngPkg.cache.ngccProcessingCache, | ||
ngPkg.cache.moduleResolutionCache, | ||
); | ||
node.data = { entryPoint, destinationFiles }; | ||
node.state = 'dirty'; | ||
ngPkg.dependsOn(node); | ||
|
@@ -134,9 +139,11 @@ const watchTransformFactory = ( | |
return createFileWatch(data.src, [data.dest]).pipe( | ||
tap(fileChange => { | ||
const { filePath, event } = fileChange; | ||
const { sourcesFileCache } = cache; | ||
const { sourcesFileCache, ngccProcessingCache } = cache; | ||
const cachedSourceFile = sourcesFileCache.get(filePath); | ||
|
||
ngccProcessingCache.clear(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need to clear the cache on each file file? We don't watch node_modules therefore I assume that we shouldn't do this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wanted to avoid a situation where There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this will not work because of the "local" processed files cache There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
if (!cachedSourceFile) { | ||
if (event === 'unlink' || event === 'add') { | ||
cache.globCache = regenerateGlobCache(sourcesFileCache); | ||
|
@@ -147,27 +154,35 @@ const watchTransformFactory = ( | |
const { declarationFileName } = cachedSourceFile; | ||
|
||
const uriToClean = [filePath, declarationFileName].map(x => fileUrl(ensureUnixPath(x))); | ||
let nodesToClean = graph.filter(node => uriToClean.some(uri => uri === node.url)); | ||
const nodesToClean = graph.filter(node => uriToClean.some(uri => uri === node.url)); | ||
|
||
nodesToClean = flatten([ | ||
...nodesToClean, | ||
// if a non ts file changes we need to clean up it's direct dependees | ||
// this is mainly done for resources such as html and css | ||
...nodesToClean.filter(x => !x.url.endsWith('.ts')).map(x => x.dependees), | ||
]); | ||
const allUrlsToClean = new Set<string>( | ||
flatten([ | ||
...nodesToClean.map(node => node.url), | ||
// if a non ts file changes we need to clean up its direct dependees | ||
// this is mainly done for resources such as html and css | ||
...nodesToClean | ||
.filter(node => !node.url.endsWith('.ts')) | ||
.map(node => node.dependees.map(dependee => dependee.url)), | ||
]), | ||
); | ||
|
||
// delete node that changes | ||
nodesToClean.forEach(node => { | ||
sourcesFileCache.delete(fileUrlPath(node.url)); | ||
allUrlsToClean.forEach(url => { | ||
sourcesFileCache.delete(fileUrlPath(url)); | ||
}); | ||
|
||
const entryPoints: EntryPointNode[] = graph.filter(isEntryPoint); | ||
entryPoints.forEach(entryPoint => { | ||
const isDirty = entryPoint.dependents.some(x => nodesToClean.some(node => node.url === x.url)); | ||
const isDirty = entryPoint.dependents.some(dependent => allUrlsToClean.has(dependent.url)); | ||
if (isDirty) { | ||
entryPoint.state = 'dirty'; | ||
const { metadata } = entryPoint.data.destinationFiles; | ||
sourcesFileCache.delete(metadata); | ||
|
||
uriToClean.forEach(url => { | ||
entryPoint.cache.analysesSourcesFileCache.delete(fileUrlPath(url)); | ||
}); | ||
} | ||
}); | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import * as ajv from 'ajv'; | ||
import { NgPackageConfig } from '../../ng-package.schema'; | ||
import * as log from '../utils/log'; | ||
|
||
/** Lazily initialized ajv validator instance. */ | ||
let ajvValidator: ajv.ValidateFunction | null = null; | ||
JoostK marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/** | ||
* Validates the `ngPackageJson` value against the JSON schema using ajv. An error is thrown if | ||
* schema errors are found. | ||
* | ||
* @param ngPackageJson The value to validate. | ||
*/ | ||
export function validateNgPackageSchema(ngPackageJson: unknown): asserts ngPackageJson is NgPackageConfig { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
const validate = getNgPackageSchemaValidator(); | ||
const isValid = validate(ngPackageJson); | ||
if (!isValid) { | ||
throw new Error( | ||
`Configuration doesn't match the required schema.\n${formatSchemaValidationErrors(validate.errors)}`, | ||
); | ||
} | ||
} | ||
|
||
function formatSchemaValidationErrors(errors: ajv.ErrorObject[]): string { | ||
return errors | ||
.map(err => { | ||
let message = `Data path ${JSON.stringify(err.dataPath)} ${err.message}`; | ||
if (err.keyword === 'additionalProperties') { | ||
message += ` (${(err.params as any).additionalProperty})`; | ||
} | ||
|
||
return message + '.'; | ||
}) | ||
.join('\n'); | ||
} | ||
|
||
/** | ||
* Returns an initialized ajv validator for the ng-package JSON schema. | ||
*/ | ||
function getNgPackageSchemaValidator(): ajv.ValidateFunction { | ||
if (ajvValidator !== null) { | ||
JoostK marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return ajvValidator; | ||
} | ||
|
||
const _ajv: ajv.Ajv = ajv({ | ||
schemaId: 'auto', | ||
useDefaults: true, | ||
jsonPointers: true, | ||
}); | ||
|
||
const ngPackageSchemaJson = require('../../ng-package.schema.json'); | ||
ajvValidator = _ajv.compile(ngPackageSchemaJson); | ||
|
||
// Add handler for x-deprecated fields | ||
_ajv.addKeyword('x-deprecated', { | ||
validate: (schema, _data, _parentSchema, _dataPath, _parentDataObject, propertyName) => { | ||
if (schema) { | ||
log.warn(`Option "${propertyName}" is deprecated${typeof schema == 'string' ? ': ' + schema : '.'}`); | ||
} | ||
|
||
return true; | ||
}, | ||
errors: false, | ||
}); | ||
|
||
return ajvValidator; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NIT: Why not simply use a
Set
directly?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the documentation aspect of having the dedicated class (JS docblock, method names, etc.). It also provides type-safety with respect to other
Set
instances, so that's nice as well.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While you have a point with docs for methods which are not added here :P.
I don't think it adds anything from a type-safety perspective, both the below are equity type-safe, unless I am missing something.
I don't have a very strong option about this and you can keep it as is. But I do think that in this case using a class doesn't add any value, especially for an internal implementation on top of that which is used in a single place.
Also, while in this case they are not in use, wrapping the Set with a Class has also some disadvantages where you don't expose all the methods and properties available for it. Example, if you need to use
.size
orentries
, you'd need to implement another method like such https://github.com/ng-packagr/ng-packagr/blob/master/src/lib/file-system/file-cache.tsThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I meant by type-safety is the fact that two
Set
s can easily be interchanged, which is not the case with two distinct implementations. I phrased the earlier comment quite poorly, hopefully this makes more sense. It's similar to theAbsoluteFsPath
branded type that's used in the compiler; although it is a string it does offer identification/documentation and more type-safety compared to just using a string.😅 I was mainly referring to the docblock on the class itself.
I see what you're saying from an internal implementation perspective and the fact that this thing is used in so few places. I still like the insight in brings when just glancing over the code, and e.g. the ability to ask an IDE where things are marked as processed (which wouldn't be as easy if this were just
Set
).