Skip to content

Commit 8a7cbc8

Browse files
author
Mohamed Koubaa
committed
codegen optimization plan
1 parent 7554cdd commit 8a7cbc8

File tree

8 files changed

+336
-32
lines changed

8 files changed

+336
-32
lines changed

AGENTS.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ Assume an appropriate virtual environment is activated. If it isn't, just abort.
33

44
## Agent Hints
55

6+
**CRITIAL: Ensure working directory**. Sometimes agents will change working directories as part of tasks and forget to change them back.
7+
pydyna agent documentation and scripts are often sensitive to working directory, so recommended to check the pwd before each agent operation
8+
if unsure.
9+
610
**CRITICAL: Never redirect output to /dev/null on Windows**. This triggers a VS Code security prompt that halts execution. Instead:
711
- For commands where you want to suppress output: Use `>$null 2>&1` (PowerShell) or just run the command without redirection
812
- For commands where you want to check output: Use `python codegen/generate.py 2>&1 | Out-Null` or capture in a variable
@@ -13,13 +17,16 @@ Assume an appropriate virtual environment is activated. If it isn't, just abort.
1317
- ✅ GOOD: `python codegen/generate.py` (no redirection)
1418
- ✅ GOOD: `$output = python codegen/generate.py 2>&1`
1519

16-
**Documentation builds**: To build docs without examples, use:
20+
**Documentation builds**: To build docs without examples:
21+
22+
Ensure that the python3.13 environment is active, then:
23+
1724
```bash
1825
# Build docs without examples or autokeywords (fast)
19-
cd doc && BUILD_EXAMPLES=false BUILD_AUTOKEYWORDS_API=false ./make.bat html
26+
BUILD_EXAMPLES=false BUILD_AUTOKEYWORDS_API=false ./doc/make.bat html
2027

2128
# Build docs with autokeywords but no examples (slow, ~8+ min for keyword imports alone)
22-
cd doc && BUILD_EXAMPLES=false BUILD_AUTOKEYWORDS_API=true ./make.bat html
29+
BUILD_EXAMPLES=false BUILD_AUTOKEYWORDS_API=true ./doc/make.bat html
2330
```
2431

2532
## Agent Coding Style Preferences

codegen/generate.py

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,18 @@ def skip_generate_keyword_class(keyword: str) -> bool:
9898
return False
9999

100100

101-
def get_undefined_alias_keywords(keywords_list: typing.List[typing.Dict]) -> typing.List[typing.Dict]:
101+
def get_undefined_alias_keywords(keywords_list: typing.List[typing.Dict], subset_domains: typing.Optional[typing.List[str]] = None) -> typing.List[typing.Dict]:
102+
from keyword_generation.utils.domain_mapper import get_keyword_domain
103+
102104
undefined_aliases: typing.List[typing.Dict] = []
103105
for alias, kwd in data_model.ALIAS_TO_KWD.items():
104106
if alias not in [kwd["name"] for kwd in keywords_list]:
107+
# Filter by subset domains if specified
108+
if subset_domains:
109+
domain = get_keyword_domain(alias)
110+
if domain not in subset_domains:
111+
continue
112+
105113
fixed_keyword = fix_keyword(alias).lower()
106114
classname = get_classname(fixed_keyword)
107115
fixed_base_keyword = fix_keyword(kwd).lower()
@@ -251,20 +259,31 @@ def generate_autodoc_file(autodoc_output_path, all_keywords, env):
251259
logger.info(f"Generated index.rst with {len(categories)} category links")
252260

253261

254-
def get_keywords_to_generate(kwd_name: typing.Optional[str] = None) -> typing.List[typing.Dict]:
262+
def get_keywords_to_generate(kwd_name: typing.Optional[str] = None, subset_domains: typing.Optional[typing.List[str]] = None) -> typing.List[typing.Dict]:
255263
"""Get keywords to generate. If a kwd name is not none, only generate
256-
it and its generations."""
264+
it and its generations. If subset_domains is provided, only generate keywords
265+
from those domains (e.g., ['boundary', 'contact', 'control'])."""
257266
assert data_model.KWDM_INSTANCE is not None, "KWDM_INSTANCE not initialized"
258267
keywords = []
259268
kwd_list = data_model.KWDM_INSTANCE.get_keywords_list()
260269

261270
# first get all aliases
262271
add_aliases(kwd_list)
263272

273+
# Import domain mapper to properly determine keyword domain
274+
from keyword_generation.utils.domain_mapper import get_keyword_domain
275+
264276
# then get keywords to generate
265277
for keyword in kwd_list:
266278
if kwd_name != None and keyword != kwd_name:
267279
continue
280+
281+
# Filter by subset domains if specified
282+
if subset_domains:
283+
domain = get_keyword_domain(keyword)
284+
if domain not in subset_domains:
285+
continue
286+
268287
for keyword, keyword_options in get_generations(keyword):
269288
item = get_keyword_item(keyword)
270289
item["options"] = keyword_options
@@ -273,16 +292,19 @@ def get_keywords_to_generate(kwd_name: typing.Optional[str] = None) -> typing.Li
273292
return keywords
274293

275294

276-
def generate_classes(lib_path: str, kwd_name: typing.Optional[str] = None, autodoc_output_path: str = "") -> None:
295+
def generate_classes(lib_path: str, kwd_name: typing.Optional[str] = None, autodoc_output_path: str = "", subset_domains: typing.Optional[typing.List[str]] = None) -> None:
277296
"""Generates the keyword classes, importer, and type-mapper
278297
if kwd_name is not None, this only generates that particular keyword class
298+
if subset_domains is not None, only generates keywords from those domains
279299
"""
280-
logger.debug(f"Starting class generation with lib_path={lib_path}, kwd_name={kwd_name}")
300+
logger.debug(f"Starting class generation with lib_path={lib_path}, kwd_name={kwd_name}, subset_domains={subset_domains}")
301+
if subset_domains:
302+
logger.info(f"Subset mode: generating only domains {subset_domains}")
281303
autodoc_entries = []
282304
env = Environment(loader=get_loader(), trim_blocks=True, lstrip_blocks=True)
283305
output_manager = OutputManager(lib_path)
284306
# Generate only requested keyword(s)
285-
keywords_list = get_keywords_to_generate(kwd_name)
307+
keywords_list = get_keywords_to_generate(kwd_name, subset_domains)
286308
logger.info(f"Generating {len(keywords_list)} keyword classes")
287309
generated_count = 0
288310
skipped_count = 0
@@ -304,9 +326,9 @@ def generate_classes(lib_path: str, kwd_name: typing.Optional[str] = None, autod
304326

305327
# Always rewrite autodoc for all keywords
306328
if autodoc_output_path and not kwd_name:
307-
all_keywords = get_keywords_to_generate()
329+
all_keywords = get_keywords_to_generate(subset_domains=subset_domains)
308330
generate_autodoc_file(autodoc_output_path, all_keywords, env)
309-
keywords_list.extend(get_undefined_alias_keywords(keywords_list))
331+
keywords_list.extend(get_undefined_alias_keywords(keywords_list, subset_domains))
310332
if kwd_name == None:
311333
generate_entrypoints(env, output_manager, keywords_list)
312334

@@ -349,23 +371,35 @@ def run_codegen(args):
349371
return
350372
load_inputs(this_folder, args)
351373

374+
# Handle subset domains
375+
subset_domains = None
376+
if args.subset:
377+
subset_domains = [d.strip() for d in args.subset.split(",")]
378+
logger.info(f"Subset mode enabled: generating only {subset_domains} domains")
379+
352380
# Handle autodoc-only mode
353381
if args.autodoc_only:
354382
logger.info("Generating autodoc files only")
355383
env = Environment(loader=get_loader(), trim_blocks=True, lstrip_blocks=True)
356-
all_keywords = get_keywords_to_generate()
384+
all_keywords = get_keywords_to_generate(subset_domains=subset_domains)
357385
generate_autodoc_file(autodoc_path, all_keywords, env)
358386
logger.info("Autodoc generation complete")
359387
return
360388

389+
# Handle subset domains
390+
subset_domains = None
391+
if args.subset:
392+
subset_domains = [d.strip() for d in args.subset.split(",")]
393+
logger.info(f"Subset mode enabled: generating only {subset_domains} domains")
394+
361395
if args.keyword == "":
362396
kwd = None
363-
logger.info("Generating code for all keywords")
364-
generate_classes(output, autodoc_output_path=autodoc_path)
397+
logger.info("Generating code for all keywords" if not subset_domains else f"Generating subset: {subset_domains}")
398+
generate_classes(output, autodoc_output_path=autodoc_path, subset_domains=subset_domains)
365399
else:
366400
kwd = args.keyword
367401
logger.info(f"Generating code for {kwd}")
368-
generate_classes(output, kwd, autodoc_output_path=autodoc_path)
402+
generate_classes(output, kwd, autodoc_output_path=autodoc_path, subset_domains=subset_domains)
369403

370404

371405
def parse_args():
@@ -419,6 +453,12 @@ def parse_args():
419453
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
420454
help="Set the logging level. Defaults to INFO.",
421455
)
456+
parser.add_argument(
457+
"--subset",
458+
"-s",
459+
default="",
460+
help="Generate only a subset of keyword domains (comma-delimited list, e.g., 'boundary,contact,control'). Useful for fast iteration during optimization work.",
461+
)
422462
return parser.parse_args()
423463

424464

doc/Makefile

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33

44
# You can set these variables from the command line, and also
55
# from the environment for the first two.
6-
SPHINXOPTS = -j auto
6+
SPHINXJOBS ?= auto
7+
SPHINXOPTS = -j $(SPHINXJOBS)
78
SPHINXBUILD = sphinx-build
89
SOURCEDIR = source
910
BUILDDIR = _build
@@ -24,7 +25,10 @@ keyword_classes:
2425
# Catch-all target: route all unknown targets to Sphinx using the new
2526
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
2627
%: Makefile
27-
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
28+
@echo "⏱️ Starting Sphinx build with -j $(SPHINXJOBS)..."
29+
@echo "Build started at $$(date '+%Y-%m-%d %H:%M:%S')"
30+
@time $(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
31+
@echo "Build finished at $$(date '+%Y-%m-%d %H:%M:%S')"
2832

2933
clean:
3034
rm -rf $(BUILDDIR)/*

doc/make.bat

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ set SOURCEDIR=source
1111
set BUILDDIR=_build
1212
set APIDIR=source\api
1313

14+
REM Set parallel build option (use SPHINXJOBS environment variable or default to auto)
15+
if "%SPHINXJOBS%" == "" (
16+
set SPHINXJOBS=auto
17+
)
18+
1419
if "%1" == "" goto help
1520
if "%1" == "clean" goto clean
1621
if "%1" == "pdf" goto pdf
@@ -28,7 +33,12 @@ if errorlevel 9009 (
2833
exit /b 1
2934
)
3035

31-
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
36+
echo Starting Sphinx build with -j %SPHINXJOBS%...
37+
echo Build started at %TIME%
38+
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% -j %SPHINXJOBS% %SPHINXOPTS% %O%
39+
set BUILD_EXIT_CODE=%ERRORLEVEL%
40+
echo Build finished at %TIME%
41+
exit /b %BUILD_EXIT_CODE%
3242
goto end
3343

3444
:clean

doc/profile_build.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""Profile the Sphinx documentation build."""
2+
3+
import os
4+
import sys
5+
from pathlib import Path
6+
from pyinstrument import Profiler
7+
8+
# Set environment variables before importing Sphinx
9+
os.environ['BUILD_EXAMPLES'] = 'false'
10+
os.environ['BUILD_AUTOKEYWORDS_API'] = 'true'
11+
os.environ['SPHINXJOBS'] = '1'
12+
13+
# Add source directory to path
14+
doc_dir = Path(__file__).parent
15+
source_dir = doc_dir / 'source'
16+
build_dir = doc_dir / '_build'
17+
18+
# Start profiling
19+
profiler = Profiler()
20+
profiler.start()
21+
22+
# Import and run Sphinx
23+
from sphinx.cmd.build import main as sphinx_main
24+
25+
sys.argv = [
26+
'sphinx-build',
27+
'-M', 'html',
28+
str(source_dir),
29+
str(build_dir),
30+
'-j', '1'
31+
]
32+
33+
try:
34+
sphinx_main(sys.argv[1:])
35+
finally:
36+
# Stop profiling and save results
37+
profiler.stop()
38+
39+
# Print to console
40+
print("\n" + "=" * 80)
41+
print("PROFILING RESULTS")
42+
print("=" * 80)
43+
profiler.print(show_all=False)
44+
45+
# Save HTML report
46+
html_output = build_dir / 'profile.html'
47+
with open(html_output, 'w') as f:
48+
f.write(profiler.output_html())
49+
print(f"\nHTML profile saved to: {html_output}")

doc/source/conf.py

Lines changed: 87 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@
88
from sphinx.builders.latex import LaTeXBuilder
99
from sphinx_gallery.sorting import FileNameSortKey
1010

11-
from ansys.dyna.core import __version__
11+
# Get version without importing the package (avoids triggering imports during conf.py execution)
12+
# This allows doc builds to work with subset-generated keywords
13+
try:
14+
from importlib.metadata import version as importlib_version
15+
__version__ = importlib_version("ansys-dyna-core")
16+
except Exception:
17+
__version__ = "unknown"
1218

1319
LaTeXBuilder.supported_image_types = ["image/png", "image/pdf", "image/svg+xml"]
1420

@@ -169,9 +175,12 @@
169175

170176
suppress_warnings = ["autoapi.python_import_resolution", "config.cache", "docutils"]
171177

172-
BUILD_AUTOKEYWORS_API = os.environ.get("BUILD_AUTOKEYWORS_API", "false").lower() == "true"
173-
if BUILD_AUTOKEYWORS_API:
178+
BUILD_AUTOKEYWORDS_API = os.environ.get("BUILD_AUTOKEYWORDS_API", "false").lower() == "true"
179+
180+
if BUILD_AUTOKEYWORDS_API:
174181
html_theme_options["ansys_sphinx_theme_autoapi"]["templates"] = "autoapi/"
182+
# Remove the auto-generated keywords from the ignore list
183+
html_theme_options["ansys_sphinx_theme_autoapi"]["ignore"] = []
175184

176185
BUILD_EXAMPLES = os.environ.get("BUILD_EXAMPLES", "true").lower() == "true"
177186
if BUILD_EXAMPLES:
@@ -233,4 +242,78 @@ def skip_run_subpackage(app, what, name, obj, skip, options):
233242

234243
def setup(sphinx):
235244
"""Add custom extensions to Sphinx."""
236-
sphinx.connect("autoapi-skip-member", skip_run_subpackage)
245+
sphinx.connect("autoapi-skip-member", skip_run_subpackage)
246+
247+
# Add timing instrumentation for performance profiling
248+
import time
249+
import os
250+
from pathlib import Path
251+
252+
# Create timing log file
253+
timing_log = Path(__file__).parent.parent / "_build" / "timing.log"
254+
timing_log.parent.mkdir(parents=True, exist_ok=True)
255+
256+
# Track phase timings
257+
phase_times = {}
258+
259+
def log_time(phase, duration=None):
260+
"""Log timing information to file."""
261+
if duration is None:
262+
# Start timing
263+
phase_times[phase] = time.time()
264+
msg = f"[{time.strftime('%H:%M:%S')}] Starting: {phase}\n"
265+
else:
266+
# End timing
267+
msg = f"[{time.strftime('%H:%M:%S')}] Completed: {phase} ({duration:.2f}s)\n"
268+
269+
with open(timing_log, "a") as f:
270+
f.write(msg)
271+
print(msg.strip())
272+
273+
# Event handlers
274+
def on_builder_inited(app):
275+
log_time("builder-init")
276+
log_time("overall-build")
277+
278+
def on_env_get_outdated(app, env, added, changed, removed):
279+
if "builder-init" in phase_times:
280+
log_time("builder-init", time.time() - phase_times["builder-init"])
281+
log_time("env-get-outdated")
282+
return []
283+
284+
def on_env_before_read_docs(app, env, docnames):
285+
if "env-get-outdated" in phase_times:
286+
log_time("env-get-outdated", time.time() - phase_times["env-get-outdated"])
287+
log_time("read-docs")
288+
289+
def on_doctree_resolved(app, doctree, docname):
290+
# Only log first and every 100th document to avoid spam
291+
if not hasattr(on_doctree_resolved, "count"):
292+
on_doctree_resolved.count = 0
293+
on_doctree_resolved.count += 1
294+
if on_doctree_resolved.count == 1:
295+
if "read-docs" in phase_times:
296+
log_time("read-docs", time.time() - phase_times["read-docs"])
297+
log_time("process-doctrees")
298+
299+
def on_build_finished(app, exception):
300+
if hasattr(on_doctree_resolved, "count"):
301+
if "process-doctrees" in phase_times:
302+
log_time("process-doctrees", time.time() - phase_times["process-doctrees"])
303+
if "overall-build" in phase_times:
304+
log_time("overall-build", time.time() - phase_times["overall-build"])
305+
306+
# Summary
307+
with open(timing_log, "a") as f:
308+
f.write(f"\n{'='*60}\n")
309+
f.write(f"Build completed at {time.strftime('%Y-%m-%d %H:%M:%S')}\n")
310+
if hasattr(on_doctree_resolved, "count"):
311+
f.write(f"Total documents processed: {on_doctree_resolved.count}\n")
312+
f.write(f"{'='*60}\n")
313+
314+
# Connect event handlers
315+
sphinx.connect("builder-inited", on_builder_inited)
316+
sphinx.connect("env-get-outdated", on_env_get_outdated)
317+
sphinx.connect("env-before-read-docs", on_env_before_read_docs)
318+
sphinx.connect("doctree-resolved", on_doctree_resolved)
319+
sphinx.connect("build-finished", on_build_finished)

0 commit comments

Comments
 (0)