Skip to content

Commit e295d96

Browse files
authored
Merge pull request #71 from CoreTrace/70-investigation-excessive-ci-execution-time-on-ubuntu-runners
ci: fix Ubuntu Noble compatibility & optimize CI pipeline
2 parents c20b319 + 538bfd3 commit e295d96

4 files changed

Lines changed: 147 additions & 34 deletions

File tree

.github/workflows/ci.yml

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ jobs:
4343
ninja-build ccache lld
4444
# Install LLVM and Clang 20
4545
wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add -
46-
sudo apt-add-repository "deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-20 main"
46+
UBUNTU_CODENAME=$(lsb_release -cs)
47+
sudo apt-add-repository "deb http://apt.llvm.org/${UBUNTU_CODENAME}/ llvm-toolchain-${UBUNTU_CODENAME}-20 main"
4748
sudo apt-get update
4849
sudo apt-get install -y llvm-20 llvm-20-dev clang-20 libclang-20-dev
4950
echo "LLVM_DIR=/usr/lib/llvm-20/lib/cmake/llvm" >> $GITHUB_ENV
@@ -108,12 +109,28 @@ jobs:
108109
run: ccache -s
109110

110111
# Tests
112+
- name: Restore run_test cache
113+
uses: actions/cache@v4
114+
with:
115+
path: .cache/run_test
116+
key: run-test-${{ runner.os }}-${{ hashFiles('build/stack_usage_analyzer', 'test/**', 'run_test.py') }}
117+
restore-keys: |
118+
run-test-${{ runner.os }}-
119+
120+
- name: Restore compile_ir cache
121+
uses: actions/cache@v4
122+
with:
123+
path: .cache/compile-ir
124+
key: compile-ir-${{ runner.os }}-${{ hashFiles('test/**') }}
125+
restore-keys: |
126+
compile-ir-${{ runner.os }}-
127+
111128
- name: Test Stack Usage Analyzer
112129
timeout-minutes: 45
113130
run: |
114131
TEST_JOBS="$(python3 -c 'import os; print(max(1, min(8, os.cpu_count() or 1)))')"
115132
echo "Running run_test.py with ${TEST_JOBS} job(s)"
116-
EXTRA_ANALYZER_ARGS=""
133+
EXTRA_ANALYZER_ARGS="--compile-ir-cache-dir=.cache/compile-ir"
117134
CORETRACE_RUN_TEST_EXTRA_ANALYZER_ARGS="${EXTRA_ANALYZER_ARGS}" \
118135
python3 -u run_test.py --jobs="${TEST_JOBS}"
119136

run_test.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,29 @@ class TestRunConfig:
3838
RUN_CONFIG = TestRunConfig()
3939
_CACHE_LOCK = threading.Lock()
4040
_MEM_CACHE = {}
41+
_FILE_HASH_CACHE = {}
42+
43+
def _get_file_hash(p: Path) -> str:
44+
path_str = str(p)
45+
try:
46+
st = p.stat()
47+
except OSError:
48+
return ""
49+
50+
# Use st_mtime_ns as a cache key for the hash
51+
cache_key = (path_str, st.st_mtime_ns, st.st_size)
52+
with _CACHE_LOCK:
53+
if cache_key in _FILE_HASH_CACHE:
54+
return _FILE_HASH_CACHE[cache_key]
55+
56+
try:
57+
h = hashlib.sha256(p.read_bytes()).hexdigest()
58+
except OSError:
59+
h = ""
60+
61+
with _CACHE_LOCK:
62+
_FILE_HASH_CACHE[cache_key] = h
63+
return h
4164
# Set to True while the top-level parallel check phase is running.
4265
# Prevents nested ThreadPoolExecutor creation (N² process explosion).
4366
_PARALLEL_PHASE = False
@@ -170,11 +193,9 @@ def _collect_cache_dependencies(args):
170193
candidates.add(p.resolve())
171194

172195
for p in sorted(candidates, key=lambda x: str(x)):
173-
try:
174-
st = p.stat()
175-
except OSError:
176-
continue
177-
deps.append([str(p), st.st_mtime_ns, st.st_size])
196+
h = _get_file_hash(p)
197+
if h:
198+
deps.append([str(p), h])
178199
return deps
179200

180201

scripts/ci/run_code_analysis.py

Lines changed: 72 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
import json
88
import subprocess
99
import sys
10+
import os
11+
import math
12+
import concurrent.futures
1013
from pathlib import Path
1114
from typing import Iterable
1215

@@ -380,38 +383,80 @@ def main() -> int:
380383
ensure_parent(Path(args.sarif_out))
381384
sarif_out_path = str(Path(args.sarif_out).resolve())
382385

383-
print(f"Running analyzer on {len(selected_inputs)} file(s).")
384-
cmd = analyzer_cmd(
385-
analyzer=analyzer,
386-
inputs=selected_inputs,
387-
fmt="json",
388-
compdb_path=compdb_path,
389-
base_dir=args.base_dir,
390-
extra_args=args.analyzer_arg,
391-
sarif_out=sarif_out_path,
392-
)
393-
run = subprocess.run(cmd, check=False, capture_output=True, text=True)
394-
if run.returncode != 0:
395-
if run.stdout:
396-
sys.stdout.write(run.stdout)
397-
if run.stderr:
398-
sys.stderr.write(run.stderr)
399-
return run.returncode
400-
401-
try:
402-
payload = json.loads(run.stdout)
403-
except json.JSONDecodeError as exc:
404-
print(f"Analyzer returned invalid JSON: {exc}", file=sys.stderr)
386+
jobs = int(os.environ.get("ANALYZER_JOBS", os.cpu_count() or 1))
387+
chunk_size = max(1, math.ceil(len(selected_inputs) / jobs))
388+
chunks = [selected_inputs[i:i + chunk_size] for i in range(0, len(selected_inputs), chunk_size)]
389+
390+
print(f"Running analyzer on {len(selected_inputs)} file(s) across {len(chunks)} job(s).")
391+
392+
def run_chunk(i, chunk):
393+
chunk_sarif = f"{sarif_out_path}.chunk{i}" if sarif_out_path else None
394+
395+
cmd = analyzer_cmd(
396+
analyzer=analyzer,
397+
inputs=chunk,
398+
fmt="json",
399+
compdb_path=compdb_path,
400+
base_dir=args.base_dir,
401+
extra_args=args.analyzer_arg,
402+
sarif_out=chunk_sarif,
403+
)
404+
run = subprocess.run(cmd, check=False, capture_output=True, text=True)
405+
return i, run, chunk_sarif
406+
407+
diags = []
408+
has_error = False
409+
all_sarif_files = []
410+
411+
with concurrent.futures.ThreadPoolExecutor(max_workers=jobs) as executor:
412+
futures = [executor.submit(run_chunk, i, c) for i, c in enumerate(chunks)]
413+
for fut in concurrent.futures.as_completed(futures):
414+
i, run, chunk_sarif = fut.result()
415+
416+
if chunk_sarif and os.path.exists(chunk_sarif):
417+
all_sarif_files.append(chunk_sarif)
418+
419+
if run.returncode != 0:
420+
if run.stdout:
421+
sys.stdout.write(run.stdout)
422+
if run.stderr:
423+
sys.stderr.write(run.stderr)
424+
has_error = True
425+
else:
426+
try:
427+
payload = json.loads(run.stdout)
428+
d = payload.get("diagnostics", [])
429+
if isinstance(d, list):
430+
diags.extend(d)
431+
except json.JSONDecodeError as exc:
432+
print(f"Analyzer returned invalid JSON: {exc}", file=sys.stderr)
433+
has_error = True
434+
435+
if has_error:
405436
return 2
406437

438+
if sarif_out_path and all_sarif_files:
439+
merged = None
440+
for p in all_sarif_files:
441+
with open(p, 'r') as f:
442+
try:
443+
data = json.load(f)
444+
if merged is None:
445+
merged = data
446+
else:
447+
if data.get("runs") and merged.get("runs"):
448+
merged["runs"][0].setdefault("results", []).extend(data["runs"][0].get("results", []))
449+
except json.JSONDecodeError:
450+
pass
451+
os.unlink(p)
452+
if merged:
453+
with open(sarif_out_path, 'w') as f:
454+
json.dump(merged, f)
455+
407456
if args.json_out:
408457
json_output_path = Path(args.json_out)
409458
ensure_parent(json_output_path)
410-
json_output_path.write_text(run.stdout, encoding="utf-8")
411-
412-
diags = payload.get("diagnostics", [])
413-
if not isinstance(diags, list):
414-
diags = []
459+
json_output_path.write_text(json.dumps({"diagnostics": diags}, indent=2), encoding="utf-8")
415460

416461
errors = sum(1 for d in diags if sev(d) == "ERROR")
417462
warnings = sum(1 for d in diags if sev(d) == "WARNING")

test/alloca/wrong-alloca.c

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
#include <alloca.h>
3+
#include <stdint.h>
4+
#include <stddef.h>
5+
6+
int foo(uint8_t small_size)
7+
{
8+
size_t size_allocation = (size_t)small_size * 1024;
9+
char* buff = (char*)alloca(size_allocation);
10+
11+
if (!buff)
12+
goto error;
13+
14+
return 0;
15+
16+
error:
17+
18+
return 1;
19+
}
20+
21+
// at line 9, column 25
22+
// [ !!Warn ] dynamic stack allocation detected for variable 'buff'
23+
// ↳ allocated type: i8
24+
// ↳ size of this allocation is not compile-time constant (VLA / variable alloca) and may lead to unbounded stack usage
25+
26+
// at line 9, column 25
27+
// [ !!Warn ] user-controlled alloca size for variable 'buff'
28+
// ↳ allocation performed via alloca/VLA; stack usage grows with runtime value
29+
// ↳ size is unbounded at compile time
30+
// ↳ size depends on user-controlled input (function argument or non-local value)

0 commit comments

Comments
 (0)