Skip to content

Commit a846583

Browse files
committed
Allow sanity check to pass without locking the cache
We check the sanity file once, without holding the lock, and if that fails we acquire the lock and check again. This means that if some other process is performing some long running cache operation (e.g building libc) it won't block all emscripten processes from running (e.g. compile processes won't be blocked by link process).
1 parent 9a0a4bc commit a846583

File tree

6 files changed

+49
-58
lines changed

6 files changed

+49
-58
lines changed

emcc.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -3245,17 +3245,17 @@ def consume_arg_file():
32453245
elif check_flag('--clear-cache'):
32463246
logger.info('clearing cache as requested by --clear-cache: `%s`', shared.Cache.dirname)
32473247
shared.Cache.erase()
3248-
shared.check_sanity(force=True) # this is a good time for a sanity check
3248+
shared.perform_sanity_checks() # this is a good time for a sanity check
32493249
should_exit = True
32503250
elif check_flag('--clear-ports'):
32513251
logger.info('clearing ports and cache as requested by --clear-ports')
32523252
ports.clear()
32533253
shared.Cache.erase()
3254-
shared.check_sanity(force=True) # this is a good time for a sanity check
3254+
shared.perform_sanity_checks() # this is a good time for a sanity check
32553255
should_exit = True
32563256
elif check_flag('--check'):
32573257
print(version_string(), file=sys.stderr)
3258-
shared.check_sanity(force=True)
3258+
shared.perform_sanity_checks()
32593259
should_exit = True
32603260
elif check_flag('--show-ports'):
32613261
ports.show_ports()

tests/common.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,7 @@ def set_temp_dir(self, temp_dir):
463463
def setUpClass(cls):
464464
super().setUpClass()
465465
print('(checking sanity from test runner)') # do this after we set env stuff
466-
shared.check_sanity(force=True)
466+
shared.perform_sanity_checks()
467467

468468
def setUp(self):
469469
super().setUp()

tests/test_other.py

+8
Original file line numberDiff line numberDiff line change
@@ -12290,3 +12290,11 @@ def test_bigint64array_polyfill(self):
1229012290

1229112291
for m, [v1, v2] in output['assertEquals']:
1229212292
self.assertEqual(v1, v2, msg=m)
12293+
12294+
def test_compile_with_cache_lock(self):
12295+
# Verify that, after warming the cache, running emcc does not require the cache lock.
12296+
# Previously we would acquire the lock during sanity checking (even when the check
12297+
# passed) which meant the second process here would deadlock.
12298+
self.run_process([EMCC, '-c', test_file('hello_world.c')])
12299+
with shared.Cache.lock('testing'):
12300+
self.run_process([EMCC, '-c', test_file('hello_world.c')])

tools/cache.py

+9-27
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,6 @@
1616

1717
# Permanent cache for system librarys and ports
1818
class Cache:
19-
# If EM_EXCLUSIVE_CACHE_ACCESS is true, this process is allowed to have direct
20-
# access to the Emscripten cache without having to obtain an interprocess lock
21-
# for it. Generally this is false, and this is used in the case that
22-
# Emscripten process recursively calls to itself when building the cache, in
23-
# which case the parent Emscripten process has already locked the cache.
24-
# Essentially the env. var EM_EXCLUSIVE_CACHE_ACCESS signals from parent to
25-
# child process that the child can reuse the lock that the parent already has
26-
# acquired.
27-
EM_EXCLUSIVE_CACHE_ACCESS = int(os.environ.get('EM_EXCLUSIVE_CACHE_ACCESS', '0'))
28-
2919
def __init__(self, dirname):
3020
# figure out the root directory for all caching
3121
self.dirname = Path(dirname).resolve()
@@ -37,42 +27,34 @@ def __init__(self, dirname):
3727
self.filelock_name = Path(dirname, 'cache.lock')
3828
self.filelock = filelock.FileLock(self.filelock_name)
3929

40-
def acquire_cache_lock(self):
30+
def acquire_cache_lock(self, reason):
4131
if config.FROZEN_CACHE:
4232
# Raise an exception here rather than exit_with_error since in practice this
4333
# should never happen
4434
raise Exception('Attempt to lock the cache but FROZEN_CACHE is set')
4535

46-
if not self.EM_EXCLUSIVE_CACHE_ACCESS and self.acquired_count == 0:
36+
if self.acquired_count == 0:
4737
logger.debug(f'PID {os.getpid()} acquiring multiprocess file lock to Emscripten cache at {self.dirname}')
4838
try:
4939
self.filelock.acquire(60)
5040
except filelock.Timeout:
51-
# The multiprocess cache locking can be disabled altogether by setting EM_EXCLUSIVE_CACHE_ACCESS=1 environment
52-
# variable before building. (in that case, use "embuilder.py build ALL" to prepopulate the cache)
53-
logger.warning(f'Accessing the Emscripten cache at "{self.dirname}" is taking a long time, another process should be writing to it. If there are none and you suspect this process has deadlocked, try deleting the lock file "{self.filelock_name}" and try again. If this occurs deterministically, consider filing a bug.')
41+
logger.warning(f'Accessing the Emscripten cache at "{self.dirname}" (for "{reason}") is taking a long time, another process should be writing to it. If there are none and you suspect this process has deadlocked, try deleting the lock file "{self.filelock_name}" and try again. If this occurs deterministically, consider filing a bug.')
5442
self.filelock.acquire()
5543

56-
self.prev_EM_EXCLUSIVE_CACHE_ACCESS = os.environ.get('EM_EXCLUSIVE_CACHE_ACCESS')
57-
os.environ['EM_EXCLUSIVE_CACHE_ACCESS'] = '1'
5844
logger.debug('done')
5945
self.acquired_count += 1
6046

6147
def release_cache_lock(self):
6248
self.acquired_count -= 1
6349
assert self.acquired_count >= 0, "Called release more times than acquire"
64-
if not self.EM_EXCLUSIVE_CACHE_ACCESS and self.acquired_count == 0:
65-
if self.prev_EM_EXCLUSIVE_CACHE_ACCESS:
66-
os.environ['EM_EXCLUSIVE_CACHE_ACCESS'] = self.prev_EM_EXCLUSIVE_CACHE_ACCESS
67-
else:
68-
del os.environ['EM_EXCLUSIVE_CACHE_ACCESS']
50+
if self.acquired_count == 0:
6951
self.filelock.release()
7052
logger.debug(f'PID {os.getpid()} released multiprocess file lock to Emscripten cache at {self.dirname}')
7153

7254
@contextlib.contextmanager
73-
def lock(self):
55+
def lock(self, reason):
7456
"""A context manager that performs actions in the given directory."""
75-
self.acquire_cache_lock()
57+
self.acquire_cache_lock(reason)
7658
try:
7759
yield
7860
finally:
@@ -82,7 +64,7 @@ def ensure(self):
8264
utils.safe_ensure_dirs(self.dirname)
8365

8466
def erase(self):
85-
with self.lock():
67+
with self.lock('erase'):
8668
if self.dirname.exists():
8769
for f in os.listdir(self.dirname):
8870
tempfiles.try_delete(Path(self.dirname, f))
@@ -129,7 +111,7 @@ def erase_lib(self, name):
129111
self.erase_file(self.get_lib_name(name))
130112

131113
def erase_file(self, shortname):
132-
with self.lock():
114+
with self.lock('erase: ' + shortname):
133115
name = Path(self.dirname, shortname)
134116
if name.exists():
135117
logger.info(f'deleting cached file: {name}')
@@ -153,7 +135,7 @@ def get(self, shortname, creator, what=None, force=False):
153135
# should never happen
154136
raise Exception(f'FROZEN_CACHE is set, but cache file is missing: "{shortname}" (in cache root path "{self.dirname}")')
155137

156-
with self.lock():
138+
with self.lock(shortname):
157139
if cachename.exists() and not force:
158140
return str(cachename)
159141
if what is None:

tools/ports/__init__.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ def fetch_project(name, url, subdir, sha512hash=None):
175175
if local_ports:
176176
logger.warning('using local ports: %s' % local_ports)
177177
local_ports = [pair.split('=', 1) for pair in local_ports.split(',')]
178-
with shared.Cache.lock():
178+
with shared.Cache.lock('local ports'):
179179
for local in local_ports:
180180
if name == local[0]:
181181
path = local[1]
@@ -244,7 +244,7 @@ def up_to_date():
244244

245245
# main logic. do this under a cache lock, since we don't want multiple jobs to
246246
# retrieve the same port at once
247-
with shared.Cache.lock():
247+
with shared.Cache.lock('unpack port'):
248248
if os.path.exists(fullpath):
249249
# Another early out in case another process build the library while we were
250250
# waiting for the lock

tools/shared.py

+26-25
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ def perform_sanity_checks():
374374

375375

376376
@ToolchainProfiler.profile()
377-
def check_sanity(force=False):
377+
def check_sanity():
378378
"""Check that basic stuff we need (a JS engine to compile, Node.js, and Clang
379379
and LLVM) exists.
380380
@@ -383,19 +383,14 @@ def check_sanity(force=False):
383383
EM_CONFIG (so, we re-check sanity when the settings are changed). We also
384384
re-check sanity and clear the cache when the version changes.
385385
"""
386-
if not force and os.environ.get('EMCC_SKIP_SANITY_CHECK') == '1':
386+
if os.environ.get('EMCC_SKIP_SANITY_CHECK') == '1':
387387
return
388388

389389
# We set EMCC_SKIP_SANITY_CHECK so that any subprocesses that we launch will
390390
# not re-run the tests.
391391
os.environ['EMCC_SKIP_SANITY_CHECK'] = '1'
392392

393-
if DEBUG:
394-
force = True
395-
396393
if config.FROZEN_CACHE:
397-
if force:
398-
perform_sanity_checks()
399394
return
400395

401396
if os.environ.get('EM_IGNORE_SANITY'):
@@ -405,31 +400,37 @@ def check_sanity(force=False):
405400
expected = generate_sanity()
406401

407402
sanity_file = Cache.get_path('sanity.txt')
408-
with Cache.lock():
403+
404+
def sanity_is_correct():
409405
if os.path.exists(sanity_file):
410406
sanity_data = utils.read_file(sanity_file)
411-
if sanity_data != expected:
412-
logger.debug('old sanity: %s' % sanity_data)
413-
logger.debug('new sanity: %s' % expected)
414-
logger.info('(Emscripten: config changed, clearing cache)')
415-
Cache.erase()
416-
# the check actually failed, so definitely write out the sanity file, to
417-
# avoid others later seeing failures too
418-
force = False
419-
else:
420-
if force:
421-
logger.debug(f'sanity file up-to-date but check forced: {sanity_file}')
422-
else:
423-
logger.debug(f'sanity file up-to-date: {sanity_file}')
424-
return # all is well
407+
if sanity_data == expected:
408+
logger.debug(f'sanity file up-to-date: {sanity_file}')
409+
return True # all is well
410+
return False
411+
412+
if sanity_is_correct():
413+
# Early return without taking the cache lock
414+
return
415+
416+
with Cache.lock('sanity'):
417+
# Check again once the cache lock as aquired
418+
if sanity_is_correct():
419+
return
420+
421+
if os.path.exists(sanity_file):
422+
sanity_data = utils.read_file(sanity_file)
423+
logger.debug('old sanity: %s' % sanity_data)
424+
logger.debug('new sanity: %s' % expected)
425+
logger.info('(Emscripten: config changed, clearing cache)')
426+
Cache.erase()
425427
else:
426428
logger.debug(f'sanity file not found: {sanity_file}')
427429

428430
perform_sanity_checks()
429431

430-
if not force:
431-
# Only create/update this file if the sanity check succeeded, i.e., we got here
432-
utils.write_file(sanity_file, expected)
432+
# Only create/update this file if the sanity check succeeded, i.e., we got here
433+
utils.write_file(sanity_file, expected)
433434

434435

435436
# Some distributions ship with multiple llvm versions so they add

0 commit comments

Comments
 (0)