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

Add entrypoint tracing #25538

Merged
merged 41 commits into from Aug 16, 2021
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
5ddd237
Add ncc'd nft
ijjk May 27, 2021
7645c51
Add initial page tracing plugin
ijjk May 27, 2021
2c8df87
Include dynamic chunks and add outputting traces
ijjk May 28, 2021
963a210
Merge remote-tracking branch 'upstream/canary' into add/nft
ijjk May 28, 2021
6d4bd43
add check
ijjk May 28, 2021
5b1ed01
check name
ijjk May 28, 2021
11d1c38
Merge remote-tracking branch 'upstream/canary' into add/nft
ijjk May 28, 2021
61fb021
remove todo and clean up ignores
ijjk May 28, 2021
03f1235
Merge remote-tracking branch 'upstream/canary' into add/nft
ijjk Jun 16, 2021
3b5c026
Merge remote-tracking branch 'upstream/canary' into add/nft
ijjk Jun 26, 2021
9376530
update compiled
ijjk Jun 26, 2021
20aafbc
Add initial test for traces
ijjk Jun 26, 2021
8b69270
Update test for webpack 4
ijjk Jun 26, 2021
7419cd7
Merge remote-tracking branch 'upstream/canary' into add/nft
ijjk Jul 7, 2021
3ab3cd9
apply suggestions from review
ijjk Jul 7, 2021
02cfd01
only create traces with webpack 5
ijjk Jul 7, 2021
21f1752
lint-fix
ijjk Jul 7, 2021
71ccfd2
normalize test on windows
ijjk Jul 7, 2021
3c1e604
normalize files before output
ijjk Jul 7, 2021
8ae2785
update test
ijjk Jul 7, 2021
b2a6b41
Merge remote-tracking branch 'upstream/canary' into add/nft
ijjk Jul 12, 2021
0dbf716
Update plugin and add more tests
ijjk Jul 12, 2021
620121f
Merge remote-tracking branch 'upstream/canary' into add/nft
ijjk Jul 12, 2021
1631552
Merge remote-tracking branch 'upstream/canary' into add/nft
ijjk Jul 21, 2021
e401004
Apply suggestions from code review
ijjk Jul 21, 2021
7cab94e
Merge branch 'add/nft' of github.com:ijjk/next.js into add/nft
ijjk Jul 21, 2021
a2fb21f
Update tests
ijjk Jul 21, 2021
010ee05
Use relative paths for traces
ijjk Jul 21, 2021
a0138e9
Merge remote-tracking branch 'upstream/canary' into add/nft
ijjk Jul 21, 2021
cbd1b3a
Merge branch 'canary' into add/nft
ijjk Jul 21, 2021
4829836
Merge remote-tracking branch 'upstream/canary' into add/nft
ijjk Jul 21, 2021
0aa38fd
Merge branch 'add/nft' of github.com:ijjk/next.js into add/nft
ijjk Jul 21, 2021
98daa68
Merge remote-tracking branch 'upstream/canary' into add/nft
ijjk Aug 6, 2021
55111a3
Add experimental flag and include/exclude config
ijjk Aug 6, 2021
1f1f6e3
ncc glob
ijjk Aug 6, 2021
076d001
Merge remote-tracking branch 'upstream/canary' into add/nft
ijjk Aug 6, 2021
8bae9aa
update precompiled
ijjk Aug 6, 2021
b0133b8
Merge remote-tracking branch 'upstream/canary' into add/nft
ijjk Aug 12, 2021
5cfee37
Merge remote-tracking branch 'upstream/canary' into add/nft
ijjk Aug 16, 2021
9823923
Add excludes to nft ignore
ijjk Aug 16, 2021
2997ee2
Merge branch 'canary' into add/nft
ijjk Aug 16, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/next/build/webpack-config.ts
Expand Up @@ -38,6 +38,7 @@ import BuildStatsPlugin from './webpack/plugins/build-stats-plugin'
import ChunkNamesPlugin from './webpack/plugins/chunk-names-plugin'
import { JsConfigPathsPlugin } from './webpack/plugins/jsconfig-paths-plugin'
import { DropClientPage } from './webpack/plugins/next-drop-client-page-plugin'
import { TraceEntryPointsPlugin } from './webpack/plugins/next-trace-entrypoints-plugin'
import NextJsSsrImportPlugin from './webpack/plugins/nextjs-ssr-import'
import NextJsSSRModuleCachePlugin from './webpack/plugins/nextjs-ssr-module-cache'
import PagesManifestPlugin from './webpack/plugins/pages-manifest-plugin'
Expand Down Expand Up @@ -1163,6 +1164,10 @@ export default async function getBaseWebpackConfig(
pagesDir,
}),
!isServer && new DropClientPage(),
!isLikeServerless &&
isServer &&
!dev &&
new TraceEntryPointsPlugin({ appDir: dir }),
// Moment.js is an extremely popular library that bundles large locale files
// by default due to how Webpack interprets its code. This is a practical
// solution that requires the user to opt into importing specific locales.
Expand Down
238 changes: 238 additions & 0 deletions packages/next/build/webpack/plugins/next-trace-entrypoints-plugin.ts
@@ -0,0 +1,238 @@
import nodePath from 'path'
import { nodeFileTrace } from 'next/dist/compiled/@vercel/nft'
import {
webpack,
isWebpack5,
sources,
} from 'next/dist/compiled/webpack/webpack'
import { TRACE_OUTPUT_VERSION } from '../../../next-server/lib/constants'

const PLUGIN_NAME = 'TraceEntryPointsPlugin'
const TRACE_IGNORES = [
'**/*/node_modules/react/**/*.development.js',
'**/*/node_modules/react-dom/**/*.development.js',
]

function getChunkGroupFromBlock(
compilation: any,
block: any
): webpack.compilation.ChunkGroup {
if (isWebpack5) {
return compilation.chunkGraph.getBlockChunkGroup(block)
}

return block.chunkGroup
}

function getModuleFromDependency(
compilation: any,
dep: any
): webpack.Module & { resource?: string } {
if (isWebpack5) {
return compilation.moduleGraph.getModule(dep)
}

return dep.module
}

export class TraceEntryPointsPlugin implements webpack.Plugin {
private appDir: string
private entryTraces: Map<string, string[]>
private entryModMap: Map<string, any>
private entryNameMap: Map<string, string>

constructor({ appDir }: { appDir: string }) {
this.appDir = appDir
this.entryTraces = new Map()
this.entryModMap = new Map()
this.entryNameMap = new Map()
}

createTraceAssets(compilation: any, assets: any) {
const namePathMap = new Map<string, string>()

this.entryNameMap.forEach((value, key) => {
namePathMap.set(value, key)
})

for (const entrypoint of compilation.entrypoints.values()) {
const entryFiles = new Set<string>()
const entryMod = this.entryModMap.get(namePathMap.get(entrypoint.name)!)

entrypoint.getFiles().forEach((file: string) => {
if (!file.endsWith(entrypoint.name + '.js')) {
entryFiles.add(nodePath.join(compilation.outputOptions.path, file))
}
})

for (const block of entryMod.blocks) {
ijjk marked this conversation as resolved.
Show resolved Hide resolved
const chunkGroup = getChunkGroupFromBlock(compilation, block)

if (!chunkGroup) {
continue
}

for (const chunk of (chunkGroup as any).chunks || []) {
chunk.files?.forEach((file: string) => {
entryFiles.add(nodePath.join(compilation.outputOptions.path, file))
})
}
}

assets[
`${isWebpack5 ? '../' : ''}${entrypoint.name}.nft.json`
ijjk marked this conversation as resolved.
Show resolved Hide resolved
] = new sources.RawSource(
JSON.stringify({
version: TRACE_OUTPUT_VERSION,
files: [...entryFiles, ...this.entryTraces.get(entrypoint.name)!],
})
)
}
}

apply(compiler: webpack.Compiler) {
if (isWebpack5) {
compiler.hooks.make.tap('NextJsPagesManifest', (compilation) => {
ijjk marked this conversation as resolved.
Show resolved Hide resolved
// @ts-ignore TODO: Remove ignore when webpack 5 is stable
compilation.hooks.processAssets.tap(
{
name: 'NextJsPagesManifest',
// @ts-ignore TODO: Remove ignore when webpack 5 is stable
stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS,
ijjk marked this conversation as resolved.
Show resolved Hide resolved
},
(assets: any) => {
this.createTraceAssets(compilation, assets)
}
)
})
} else {
compiler.hooks.emit.tap('NextJsPagesManifest', (compilation: any) => {
ijjk marked this conversation as resolved.
Show resolved Hide resolved
this.createTraceAssets(compilation, compilation.assets)
})
}

compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
compilation.hooks.finishModules.tapAsync(
PLUGIN_NAME,
async (_stats: any, callback: any) => {
try {
const depModMap = new Map<string, any>()

compilation.entries.forEach((entry) => {
const name = entry.name || entry.options?.name

if (name?.startsWith('pages/') && entry.dependencies[0]) {
Copy link
Member

Choose a reason for hiding this comment

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

Here you should iterate over all entry.dependencies to handle cases like entry: { "pages/a": ["./pages/a.js", "./something-else.js"] }

const entryMod = getModuleFromDependency(
compilation,
entry.dependencies[0]
)

if (entryMod.resource) {
this.entryNameMap.set(entryMod.resource, name)
this.entryModMap.set(entryMod.resource, entryMod)
}
}
})

const readFile = (path: string) => {
const mod = depModMap.get(path) || this.entryModMap.get(path)

// map the transpiled source when available to avoid
// parse errors in node-file-trace
if (mod?._source?._valueAsBuffer) {
return mod._source._valueAsBuffer
}

try {
return compilation.inputFileSystem.readFileSync(path)
Copy link
Member

Choose a reason for hiding this comment

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

Reading the files directly from fs is probably not the best idea.

A loader could have modified the source code after filesystem. Or it could be transpiled from a language nft doesn't understand.

There is a NormalModule.originalSource() function which gives you the source code for a module after loader processing. That's in a language webpack understands, so either javascript, or something else you can ignore. Best check Module.type for javascript/* to only process JS code.

Note that some modules might not end up at all in the output, so best use the list of chunks from createTraceAssets and grab all modules from there, de-duplicate them, analyse them with nft for more references, merge and cache the results per chunk and add them to the analysis data while iterating over the chunks.

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 looks like we can't use the list of files from createTraceAssets during this hook since we need it to be immediately after transpiling with babel and compilation.entrypoints.values() seems to be empty at this stage. I added some additional tests and gathering the files from the entry's module dependencies seems to gather everything we need currently and allows us to use the dependencies source if available as well.

} catch (e) {
if (e.code === 'ENOENT' || e.code === 'EISDIR') {
return null
}
throw e
}
}
const readlink = (path: string) => {
try {
return compilation.inputFileSystem.readlinkSync(path)
Copy link
Member

Choose a reason for hiding this comment

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

sync fs calls might have a performance influence...

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 seems node-file-trace currently relies on sync fs calls, ideally we could get these from the webpack cache so that it's not actually hitting the filesystem for most of these calls

} catch (e) {
if (
e.code !== 'EINVAL' &&
e.code !== 'ENOENT' &&
e.code !== 'UNKNOWN'
) {
throw e
}
return null
}
}
const stat = (path: string) => {
try {
return compilation.inputFileSystem.statSync(path)
} catch (e) {
if (e.code === 'ENOENT') {
return null
}
throw e
}
}

const nftCache = {}
const entryPaths = Array.from(this.entryModMap.keys())

for (const entry of entryPaths) {
depModMap.clear()
const entryMod = this.entryModMap.get(entry)
const cachedTraces = entryMod.buildInfo?.cachedNextEntryTrace
Copy link
Member

Choose a reason for hiding this comment

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

That's too naive caching. Even if the module is unchanged it could be that files that are only read by nft have changed. It's not safe that way.

I would omit that for this PR and add caching in a separate PR.

We would need to track all files accessed by nft (good that it exposes the fs access methods), create a snapshot from that, store the traces and the snapshot and validate the snapshot on restore.

Copy link
Member Author

Choose a reason for hiding this comment

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

Commented out the caching for now noting why it's disabled, will investigate it more in a follow-up PR


if (isWebpack5 && cachedTraces) {
this.entryTraces.set(
this.entryNameMap.get(entry)!,
cachedTraces
)
continue
}

for (const dep of entryMod.dependencies) {
Copy link
Member

Choose a reason for hiding this comment

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

I don't really understand what's going on here.

Why is the module graph followed only one level? What's about dependencies of dependencies?

Is this piece of code even needed?

Copy link
Member Author

Choose a reason for hiding this comment

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

We need to be able to trace the transpiled version of any imported files e.g. import util from '../util', I added iterating over all nested dependencies

const depMod = getModuleFromDependency(compilation, dep)

if (depMod?.resource) {
depModMap.set(depMod.resource, depMod)
}
}

const toTrace: string[] = [entry, ...depModMap.keys()]

const root = nodePath.parse(process.cwd()).root
styfle marked this conversation as resolved.
Show resolved Hide resolved
const result = await nodeFileTrace(toTrace, {
base: root,
cache: nftCache,
processCwd: this.appDir,
readFile,
readlink,
stat,
ignore: TRACE_IGNORES,
Copy link
Member

Choose a reason for hiding this comment

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

Do we also need to ignore the user's input from unstable_excludeFiles here?

Copy link
Member Author

@ijjk ijjk Aug 16, 2021

Choose a reason for hiding this comment

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

I was currently applying this at the end of the build where unstable_includeFiles is applied in case an includeFile collided with an excludeFile we could apply it here too if it helps with performance

Copy link
Member

Choose a reason for hiding this comment

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

I suspect it will impact performance because you can stop tracing earlier so we don't look up deps of deps.

https://github.com/vercel/vercel/blob/18bec983aefbe2a77bd14eda6fca59ff7e956d8b/packages/node/src/index.ts#L204

Copy link
Member Author

Choose a reason for hiding this comment

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

Added this to the ignore config here 9823923

Copy link
Member Author

@ijjk ijjk Aug 16, 2021

Choose a reason for hiding this comment

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

Or actually it seems we can't pass this to the plugin since we need the built pages to gather the page configs 🤔 we might need to make this a next.config.js config to allow excluding while tracing

Copy link
Member

Choose a reason for hiding this comment

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

Oh I assumed thats what it was: a next.config.js config so users could exclude files

})

const tracedDeps: string[] = []

for (const file of result.fileList) {
if (result.reasons[file].type === 'initial') {
styfle marked this conversation as resolved.
Show resolved Hide resolved
continue
}
tracedDeps.push(nodePath.join(root, file))
}

entryMod.buildInfo.cachedNextEntryTrace = tracedDeps
this.entryTraces.set(this.entryNameMap.get(entry)!, tracedDeps)
}

callback()
} catch (err) {
callback(err)
}
}
)
})
}
}
7 changes: 7 additions & 0 deletions packages/next/compiled/@vercel/nft/LICENSE
@@ -0,0 +1,7 @@
Copyright 2019 Vercel, Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1 change: 1 addition & 0 deletions packages/next/compiled/@vercel/nft/index.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/next/compiled/@vercel/nft/package.json
@@ -0,0 +1 @@
{"name":"@vercel/nft","main":"index.js","license":"MIT"}
2 changes: 1 addition & 1 deletion packages/next/compiled/terser/bundle.min.js

Large diffs are not rendered by default.