Skip to content

Commit 4fbff61

Browse files
authored
Add closure type annotations to enable better DCE with external code (emscripten-core#16208)
We had a report that some common method names such as `getContext` and `contains` was being kept alive because closure was not able to determine the type on which they were be used internally.
1 parent f4115eb commit 4fbff61

File tree

9 files changed

+89
-25
lines changed

9 files changed

+89
-25
lines changed

src/Fetch.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ var Fetch = {
3232
#if FETCH_DEBUG
3333
console.log('fetch: IndexedDB upgrade needed. Clearing database.');
3434
#endif
35-
var db = event.target.result;
35+
var db = /** @type {IDBDatabase} */ (event.target.result);
3636
if (db.objectStoreNames.contains('FILES')) {
3737
db.deleteObjectStore('FILES');
3838
}
@@ -191,7 +191,7 @@ function fetchLoadCachedData(db, fetch, onsuccess, onerror) {
191191
}
192192
}
193193

194-
function fetchCacheData(db, fetch, data, onsuccess, onerror) {
194+
function fetchCacheData(/** @type {IDBDatabase} */ db, fetch, data, onsuccess, onerror) {
195195
if (!db) {
196196
#if FETCH_DEBUG
197197
console.error('fetch: IndexedDB not available!');

src/IDBStore.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
return callback(e);
3030
}
3131
req.onupgradeneeded = function(e) {
32-
var db = e.target.result;
32+
var db = /** @type {IDBDatabase} */ (e.target.result);
3333
var transaction = e.target.transaction;
3434
var fileStore;
3535
if (db.objectStoreNames.contains(IDBStore.DB_STORE_NAME)) {
@@ -39,7 +39,7 @@
3939
}
4040
};
4141
req.onsuccess = function() {
42-
db = req.result;
42+
db = /** @type {IDBDatabase} */ (req.result);
4343
// add to the cache
4444
IDBStore.dbs[name] = db;
4545
callback(null, db);

src/library_browser.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ var LibraryBrowser = {
152152
var img = new Image();
153153
img.onload = () => {
154154
assert(img.complete, 'Image ' + name + ' could not be decoded');
155-
var canvas = document.createElement('canvas');
155+
var canvas = /** @type {!HTMLCanvasElement} */ (document.createElement('canvas'));
156156
canvas.width = img.width;
157157
canvas.height = img.height;
158158
var ctx = canvas.getContext('2d');
@@ -327,7 +327,7 @@ var LibraryBrowser = {
327327
return handled;
328328
},
329329

330-
createContext: function(canvas, useWebGL, setInModule, webGLContextAttributes) {
330+
createContext: function(/** @type {HTMLCanvasElement} */ canvas, useWebGL, setInModule, webGLContextAttributes) {
331331
if (useWebGL && Module.ctx && canvas == Module.canvas) return Module.ctx; // no need to recreate GL context if it's already been created for this canvas.
332332

333333
var ctx;
@@ -946,7 +946,7 @@ var LibraryBrowser = {
946946
// Emulate setImmediate. (note: not a complete polyfill, we don't emulate clearImmediate() to keep code size to minimum, since not needed)
947947
var setImmediates = [];
948948
var emscriptenMainLoopMessageId = 'setimmediate';
949-
var Browser_setImmediate_messageHandler = function(event) {
949+
var Browser_setImmediate_messageHandler = function(/** @type {Event} */ event) {
950950
// When called in current thread or Worker, the main loop ID is structured slightly different to accommodate for --proxy-to-worker runtime listening to Worker events,
951951
// so check for both cases.
952952
if (event.data === emscriptenMainLoopMessageId || event.data.target === emscriptenMainLoopMessageId) {
@@ -1402,7 +1402,7 @@ var LibraryBrowser = {
14021402

14031403
path = PATH_FS.resolve(path);
14041404

1405-
var canvas = Module["preloadedImages"][path];
1405+
var canvas = /** @type {HTMLCanvasElement} */(Module["preloadedImages"][path]);
14061406
if (canvas) {
14071407
var ctx = canvas.getContext("2d");
14081408
var image = ctx.getImageData(0, 0, canvas.width, canvas.height);

src/library_eventloop.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ LibraryJSEventLoop = {
4444
'var __setImmediate_id_counter = 0;\n' +
4545
'var __setImmediate_queue = [];\n' +
4646
'var __setImmediate_message_id = "_si";\n' +
47-
'function __setImmediate_cb(e) {\n' +
47+
'function __setImmediate_cb(/** @type {Event} */e) {\n' +
4848
'if (e.data === __setImmediate_message_id) {\n' +
4949
'e.stopPropagation();\n' +
5050
'__setImmediate_queue.shift()();\n' +

src/library_idbfs.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ mergeInto(LibraryManager.library, {
5252
return callback("Unable to connect to IndexedDB");
5353
}
5454
req.onupgradeneeded = (e) => {
55-
var db = e.target.result;
55+
var db = /** @type {IDBDatabase} */ (e.target.result);
5656
var transaction = e.target.transaction;
5757

5858
var fileStore;
@@ -68,7 +68,7 @@ mergeInto(LibraryManager.library, {
6868
}
6969
};
7070
req.onsuccess = () => {
71-
db = req.result;
71+
db = /** @type {IDBDatabase} */ (req.result);
7272

7373
// add to the cache
7474
IDBFS.dbs[name] = db;

src/library_sdl.js

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1515,12 +1515,14 @@ var LibrarySDL = {
15151515
SDL_AudioQuit__sig: 'v',
15161516
SDL_AudioQuit: function() {
15171517
for (var i = 0; i < SDL.numChannels; ++i) {
1518-
if (SDL.channels[i].audio) {
1519-
SDL.channels[i].audio.pause();
1520-
SDL.channels[i].audio = undefined;
1518+
var chan = /** @type {{ audio: (HTMLMediaElement|undefined) }} */ (SDL.channels[i]);
1519+
if (chan.audio) {
1520+
chan.audio.pause();
1521+
chan.audio = undefined;
15211522
}
15221523
}
1523-
if (SDL.music.audio) SDL.music.audio.pause();
1524+
var audio = /** @type {HTMLMediaElement} */ (SDL.music.audio);
1525+
if (audio) audio.pause();
15241526
SDL.music.audio = undefined;
15251527
},
15261528

@@ -2989,7 +2991,7 @@ var LibrarySDL = {
29892991
Mix_HaltChannel__sig: 'ii',
29902992
Mix_HaltChannel: function(channel) {
29912993
function halt(channel) {
2992-
var info = SDL.channels[channel];
2994+
var info = /** @type {{ audio: HTMLMediaElement }} */ (SDL.channels[channel]);
29932995
if (info.audio) {
29942996
info.audio.pause();
29952997
info.audio = null;
@@ -3070,7 +3072,7 @@ var LibrarySDL = {
30703072
Mix_PauseMusic__proxy: 'sync',
30713073
Mix_PauseMusic__sig: 'v',
30723074
Mix_PauseMusic: function() {
3073-
var audio = SDL.music.audio;
3075+
var audio = /** @type {HTMLMediaElement} */ (SDL.music.audio);
30743076
if (audio) audio.pause();
30753077
},
30763078

@@ -3084,7 +3086,7 @@ var LibrarySDL = {
30843086
Mix_HaltMusic__proxy: 'sync',
30853087
Mix_HaltMusic__sig: 'i',
30863088
Mix_HaltMusic: function() {
3087-
var audio = SDL.music.audio;
3089+
var audio = /** @type {HTMLMediaElement} */ (SDL.music.audio);
30883090
if (audio) {
30893091
audio.src = audio.src; // rewind <media> element
30903092
audio.currentPosition = 0; // rewind Web Audio graph playback.
@@ -3135,6 +3137,7 @@ var LibrarySDL = {
31353137
}
31363138
return;
31373139
}
3140+
/** @type {{ audio: HTMLMediaElement }} */
31383141
var info = SDL.channels[channel];
31393142
if (info && info.audio) {
31403143
info.audio.pause();
@@ -3199,7 +3202,7 @@ var LibrarySDL = {
31993202
throw 'bad context';
32003203
}
32013204
} catch (ex) {
3202-
var canvas = document.createElement('canvas');
3205+
var canvas = /** @type {HTMLCanvasElement} */(document.createElement('canvas'));
32033206
SDL.ttfContext = canvas.getContext('2d');
32043207
}
32053208
#if ASSERTIONS

src/library_webgl.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -584,7 +584,7 @@ var LibraryGL = {
584584
},
585585
#endif
586586
// Returns the context handle to the new context.
587-
createContext: function(canvas, webGLContextAttributes) {
587+
createContext: function(/** @type {HTMLCanvasElement} */ canvas, webGLContextAttributes) {
588588
#if OFFSCREEN_FRAMEBUFFER
589589
// In proxied operation mode, rAF()/setTimeout() functions do not delimit frame boundaries, so can't have WebGL implementation
590590
// try to detect when it's ok to discard contents of the rendered backbuffer.
@@ -637,10 +637,12 @@ var LibraryGL = {
637637
// TODO: Once the bug is fixed and shipped in Safari, adjust the Safari version field in above check.
638638
if (!canvas.getContextSafariWebGL2Fixed) {
639639
canvas.getContextSafariWebGL2Fixed = canvas.getContext;
640-
canvas.getContext = function(ver, attrs) {
640+
/** @type {function(this:HTMLCanvasElement, string, (Object|null)=): (Object|null)} */
641+
function fixedGetContext(ver, attrs) {
641642
var gl = canvas.getContextSafariWebGL2Fixed(ver, attrs);
642643
return ((ver == 'webgl') == (gl instanceof WebGLRenderingContext)) ? gl : null;
643644
}
645+
canvas.getContext = fixedGetContext;
644646
}
645647
#endif
646648

tests/test_other.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7982,6 +7982,65 @@ def test_closure_cmdline_utf8_chars(self):
79827982
create_file(externs, '')
79837983
self.run_process([EMCC, test, '--closure=1', '--closure-args', '--externs "' + externs + '"'])
79847984

7985+
def test_closure_type_annotations(self):
7986+
# Verify that certain type annotations exist to allow closure to avoid
7987+
# ambiguity and maximize optimization opportunities in user code.
7988+
#
7989+
# Currently we check for a fixed set of known attribute names which
7990+
# have been reported as unannoted in the past:
7991+
attributes = ['put', 'getContext', 'contains', 'stopPropagation', 'pause']
7992+
methods = ''
7993+
for attribute in attributes:
7994+
methods += f'''
7995+
this.{attribute} = function() {{
7996+
console.error("my {attribute}");
7997+
}};
7998+
'''
7999+
create_file('pre.js', '''
8000+
/** @constructor */
8001+
function Foo() {
8002+
this.bar = function() {
8003+
console.error("my bar");
8004+
};
8005+
this.baz = function() {
8006+
console.error("my baz");
8007+
};
8008+
%s
8009+
return this;
8010+
}
8011+
8012+
function getObj() {
8013+
return new Foo();
8014+
}
8015+
8016+
var testobj = getObj();
8017+
testobj.bar();
8018+
8019+
/** Also keep alive certain library functions */
8020+
Module['keepalive'] = [_emscripten_start_fetch, _emscripten_pause_main_loop, _SDL_AudioQuit];
8021+
''' % methods)
8022+
8023+
self.build(test_file('hello_world.c'), emcc_args=[
8024+
'--closure=1',
8025+
'-sINCLUDE_FULL_LIBRARY',
8026+
'-sFETCH',
8027+
'-sFETCH_SUPPORT_INDEXEDDB',
8028+
'-sCLOSURE_WARNINGS=error',
8029+
'--pre-js=pre.js'
8030+
])
8031+
code = read_file('hello_world.js')
8032+
# `bar` method is used so should not be DCE'd
8033+
self.assertContained('my bar', code)
8034+
# `baz` method is not used
8035+
self.assertNotContained('my baz', code)
8036+
8037+
for attribute in attributes:
8038+
# None of the attributes in our list should be used either and should therefore
8039+
# be DCE'd unless there is some usage of that name within emscripten that is
8040+
# not annotated.
8041+
if 'my ' + attribute in code:
8042+
self.assertFalse('Attribute `%s` could not be DCEd' % attribute)
8043+
79858044
@with_env_modify({'EMPROFILE': '1'})
79868045
def test_toolchain_profiler(self):
79878046
# Verify some basic functionality of EMPROFILE

tools/file_packager.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -755,20 +755,20 @@ def generate_js(data_target, data_files, metadata):
755755
return errback(e);
756756
}
757757
openRequest.onupgradeneeded = function(event) {
758-
var db = event.target.result;
758+
var db = /** @type {IDBDatabase} */ (event.target.result);
759759
760-
if(db.objectStoreNames.contains(PACKAGE_STORE_NAME)) {
760+
if (db.objectStoreNames.contains(PACKAGE_STORE_NAME)) {
761761
db.deleteObjectStore(PACKAGE_STORE_NAME);
762762
}
763763
var packages = db.createObjectStore(PACKAGE_STORE_NAME);
764764
765-
if(db.objectStoreNames.contains(METADATA_STORE_NAME)) {
765+
if (db.objectStoreNames.contains(METADATA_STORE_NAME)) {
766766
db.deleteObjectStore(METADATA_STORE_NAME);
767767
}
768768
var metadata = db.createObjectStore(METADATA_STORE_NAME);
769769
};
770770
openRequest.onsuccess = function(event) {
771-
var db = event.target.result;
771+
var db = /** @type {IDBDatabase} */ (event.target.result);
772772
callback(db);
773773
};
774774
openRequest.onerror = function(error) {

0 commit comments

Comments
 (0)