Skip to content

Commit a15bb51

Browse files
committed
fix #3825: memory leak of pluginData values
1 parent f6e6481 commit a15bb51

File tree

3 files changed

+29
-21
lines changed

3 files changed

+29
-21
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232
3333
With this release, esbuild will attempt to print comments that come before case clauses in switch statements. This is similar to what esbuild already does for comments inside of certain types of expressions. Note that these types of comments are not printed if minification is enabled (specifically whitespace minification).
3434
35+
* Fix a memory leak with `pluginData` ([#3825](https://github.com/evanw/esbuild/issues/3825))
36+
37+
With this release, the build context's internal `pluginData` cache will now be cleared when starting a new build. This should fix a leak of memory from plugins that return `pluginData` objects from `onResolve` and/or `onLoad` callbacks.
38+
3539
## 0.23.0
3640

3741
**_This release deliberately contains backwards-incompatible changes._** To avoid automatically picking up releases like this, you should either be pinning the exact version of `esbuild` in your `package.json` file (recommended) or be using a version range syntax that only accepts patch upgrades such as `^0.22.0` or `~0.22.0`. See npm's documentation about [semver](https://docs.npmjs.com/cli/v6/using-npm/semver/) for more information.

cmd/esbuild/service.go

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -836,7 +836,6 @@ func (service *serviceType) convertPlugins(key int, jsPlugins interface{}, activ
836836

837837
var onResolveCallbacks []filteredCallback
838838
var onLoadCallbacks []filteredCallback
839-
hasOnStart := false
840839
hasOnEnd := false
841840

842841
filteredCallbacks := func(pluginName string, kind string, items []interface{}) (result []filteredCallback, err error) {
@@ -860,10 +859,6 @@ func (service *serviceType) convertPlugins(key int, jsPlugins interface{}, activ
860859
p := p.(map[string]interface{})
861860
pluginName := p["name"].(string)
862861

863-
if p["onStart"].(bool) {
864-
hasOnStart = true
865-
}
866-
867862
if p["onEnd"].(bool) {
868863
hasOnEnd = true
869864
}
@@ -944,22 +939,20 @@ func (service *serviceType) convertPlugins(key int, jsPlugins interface{}, activ
944939
}
945940
activeBuild.mutex.Unlock()
946941

947-
// Only register "OnStart" if needed
948-
if hasOnStart {
949-
build.OnStart(func() (api.OnStartResult, error) {
950-
response, ok := service.sendRequest(map[string]interface{}{
951-
"command": "on-start",
952-
"key": key,
953-
}).(map[string]interface{})
954-
if !ok {
955-
return api.OnStartResult{}, errors.New("The service was stopped")
956-
}
957-
return api.OnStartResult{
958-
Errors: decodeMessages(response["errors"].([]interface{})),
959-
Warnings: decodeMessages(response["warnings"].([]interface{})),
960-
}, nil
961-
})
962-
}
942+
// Always register "OnStart" to clear "pluginData"
943+
build.OnStart(func() (api.OnStartResult, error) {
944+
response, ok := service.sendRequest(map[string]interface{}{
945+
"command": "on-start",
946+
"key": key,
947+
}).(map[string]interface{})
948+
if !ok {
949+
return api.OnStartResult{}, errors.New("The service was stopped")
950+
}
951+
return api.OnStartResult{
952+
Errors: decodeMessages(response["errors"].([]interface{})),
953+
Warnings: decodeMessages(response["warnings"].([]interface{})),
954+
}, nil
955+
})
963956

964957
// Only register "OnResolve" if needed
965958
if len(onResolveCallbacks) > 0 {

lib/shared/common.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1350,6 +1350,13 @@ let handlePlugins = async (
13501350
}
13511351

13521352
requestCallbacks['on-start'] = async (id, request: protocol.OnStartRequest) => {
1353+
// Reset the "pluginData" map before each new build to avoid a memory leak.
1354+
// This is done before each new build begins instead of after each build ends
1355+
// because I believe the current API doesn't restrict when you can call
1356+
// "resolve" and there may be some uses of it that call it around when the
1357+
// build ends, and we don't want to accidentally break those use cases.
1358+
details.clear()
1359+
13531360
let response: protocol.OnStartResponse = { errors: [], warnings: [] }
13541361
await Promise.all(onStartCallbacks.map(async ({ name, callback, note }) => {
13551362
try {
@@ -1548,6 +1555,7 @@ let handlePlugins = async (
15481555
// even if the JavaScript objects aren't serializable. And we also avoid
15491556
// the overhead of serializing large JavaScript objects.
15501557
interface ObjectStash {
1558+
clear(): void
15511559
load(id: number): any
15521560
store(value: any): number
15531561
}
@@ -1556,6 +1564,9 @@ function createObjectStash(): ObjectStash {
15561564
const map = new Map<number, any>()
15571565
let nextID = 0
15581566
return {
1567+
clear() {
1568+
map.clear()
1569+
},
15591570
load(id) {
15601571
return map.get(id)
15611572
},

0 commit comments

Comments
 (0)