Skip to content

Single pass autoprof - #7

Open
TTsangSC wants to merge 8 commits into
handle-nested-importsfrom
single-pass-autoprof
Open

TTsangSC wants to merge 8 commits into
handle-nested-importsfrom
single-pass-autoprof

Conversation

@TTsangSC

@TTsangSC TTsangSC commented Sep 10, 2026 •

Copy link
Copy Markdown
Owner

This PR mainly exists for documentation/discussion purposes. See also pyutils#440, which this builds upon.

The gist is that with the previous setup (as of pyutils#440) we walk the AST in line_profiler.autoprofile 1–3 separate times:

  • ~.run_module.ImportFromTransformer walks it once to resolve relative imports, if running kernprof -m.
  • ~.profmod_extractor._ImportFinder does another walk to glean all ~._import_targets.ImportTarget covered by --prof-mod.
  • If doing full-script rewriting, ~.ast_profile_transformer.AstProfileTransformer walks the AST a final time to insert @profile decorators to functions and methods, and to profile imports missed by ~.profmod_extractor.ProfmodExtractor.extract_all() when using --prof-imports.

This resulted in code duplication, and also in bookkeeping overhead to guard against duplicating the on-import AST nodes, because some were inserted in the body of ~.ast_tree_profiler.AstTreeProfiler._profile_ast_tree(), and some in AstProfileTransformer._transform() which the former conditionally calls.

Changes

To streamline things in line_profiler.autoprofile:

  • Many of the shared components are moved to the new ~._single_pass_transformer submodule.
  • Said submodule also defines a new SinglePassTransformer class, which handles all the aforementioned AST transformations in one pass.
  • The superseded line_profiler.autoprofile components are refactored to wrap around ~._single_pass_transformer components, but are kept around for backward compatibility.
  • A new boolean switch line_profiler._diagnostics.USE_LEGACY_AUTOPROF (backed by the environment variable ${LINE_PROFILER_AUTOPROFILE_CORE}) is used so that when using ~.autoprofile.run(), one can switch between:
    • The "new"/"single-pass" core, which uses SinglePassTransformer, and
    • The "legacy"/"old"/"multi-pass" core, which uses the previous workflow.

Tests in tests/test_autoprofile.py have been updated to test both cores and their resp. components where appropriate.

Discussions/TODOs

  • Should we deprecate the old interfaces (line_profiler.autoprofile.ast_profile_transformer, .ast_tree_profiler, .profmod_extractor, and .run_module)?

  • Or maybe even just wait for version 6 to remove them outright? (This knocks about 1.1k lines out, putting our LoC at around the same level as ENH: handle nested and star imports pyutils/line_profiler#440. Further savings may be possible, since some of the ~._single_pass_transformer and ~.autoprofile components are mostly there to allow for keeping the behaviors consistent with ENH: handle nested and star imports pyutils/line_profiler#440.)

  • The single-pass approach streamlines the workflow and reduces inter-component bookkeeping which is a pain, but so far has failed to either actually

    • Reduce the codebase size (see above), or
    • Improve performance.

    For reference regarding the latter, I ran pytest -k "(test_autoprofile and not star_import and not nested_import and not ast_tree_profiler and not ast_profile_transformer and not profmod_extractor and not single_pass_transformer and not multitarget) or not test_autoprofile" to deselect all the tests directly affected by the refactor, and the timings are:

    This may be a function of how our test suite tends to use small and shallow snippets, which makes multiple AST passes inexpensive. SinglePassTransformer is probably also pretty unoptimized as-is, e.g. not having much in the way of checks against descending into a composite-statement node.

kernprof.py::_normalize_prof_nested_imports()
line_profiler/autoprof/ast_tree_profiler.py
line_profiler/autoprof/autoprofile.py
    Updated renamed/migrated imports

line_profiler/autoprofile/_single_pass_transformer.py
    New module to eventually supersede `~.ast_profile_transformer` and
    `~.ast_tree_profiler`, and `~.profmod_extractor`

line_profiler/autoprofile/ast_profile_transformer.py
    __all__
        Added to delineate the public API
    AstProfileTransformer._get_profile_imports_in()
        Refactored to depend on
        `~._single_pass_transformer._CompoundNodeChecker` instead of
        `~.profmod_extractor._ImportFinder`

line_profiler/autoprofile/profmod_extractor.py
    <General>
        Updated renamed/migrated imports
    __all__
        Added to delineate the public API
    _ImportFinder
        - Now dependent on
          `~._single_pass_transformer._CompoundNodeChecker`
        - Updated signatures of `.__init__()` and `.find()` for simpler
          use
line_profiler/autoprofile/_import_targets.py
::ImportTarget._from_ast_nodes()
    Removed now-unused method

line_profiler/autoprofile/_single_pass_transformer.py
::ContextAwareVisitor
    ._pre_visit_hook(), ._post_visit_hook()
        Renamed from the corresponding public methods
    ._current_loc, ._node_stack
        Now self-consistent when used in node-type-specific visit
        methods
    .visit(), .generic_visit()
        Updated to make sure that the above are self-consistent

line_profiler/autoprofile/ast_profile_transformer.py
::AstProfileTransformer
    <General>
        Now a `ContextAwareVisitor` subclass with simplified
        implementation
    ._should_visit_imports
        Changed typing into `Callable[[ast.AST], bool]`, using the check
        as implemented in `_CompoundNodeChecker.check()` instead of
        repeating it
    ._visit_import()
        Simplified implementation

line_profiler/autoprofile/profmod_extractor.py::_ImportFinder
    <General>
        Now a `ContextAwareVisitor` subclass with simplified
        implementations
    .found_imports
        Updated typing
    .find()
        Updated implementation
    .visit(), .visit_Import(), .visit_ImportFrom()
        Refactored and simplified from `.generic_visit()`
line_profiler/autoprofile/_single_pass_transformer.py
    should_profile_regular_import(), should_profile_star_import()
        Migrated from private functions in `~.profmod_extractor`
    _DuplicateImportChecker
        Migrated from `~.ast_profile_transformer._DuplicateChecker`
    _ConcreteDuplicateImportChecker
        New concrete implementation of the above
    SinglePassTransformer
        New `ast.NodeTransformer` class (WIP) which does what
        `~.profmod_extractor.ProfmodExtractor` and
        `~.ast_profile_transformer.AstProfileTransformer` do in one pass

line_profiler/autoprofile/ast_profile_transformer.py
::_ContextAwareDuplicateChecker
    Now a `_ConcreteDuplicateImportChecker` subclass

line_profiler/autoprofile/line_profiler_utils.py
    <General>
        Updated import path of `should_profile_regular_import()`
    __all__
        Added to delineate the public API

line_profiler/autoprofile/profmod_extractor.py
::ProfmodExtractor._find_modnames_in_tree_imports()
    Updated to use the new `should_profile_regular_import()` and
    `should_profile_star_import()`

line_profiler/rc/line_profiler.toml
::[tool.line_profiler.autoprofile]::prof_func_defs
    New switch for whether to profile locally-defined functions when
    using `line_profiler.autoprofile`

TODO:
    Refactor `AstProfileTransformer` to be a wrapper around
    `SinglePassTransformer`
line_profiler/autoprofile/ast_profile_transformer.py
::AstProfileTransformer
    <General>
        Now a `SinglePassTransformer` subclass
    .__init__()
        Refactored to be a wrapper around `super().__init__()`
        (Warning: the signatures are incompatible!)
    ._handle_new_import_target()
        New override for the superclass method to handle
        `._dropped_star_imports`
    .visit_Import(), visit_ImportFrom()
        Now thin wrappers around the superclass methods
line_profiler/_diagnostics.py::USE_LEGACY_AUTOPROF
    New boolean switch (env var: `${LINE_PROFILER_AUTOPROFILE_CORE}`)
    for whether to use the old system or the new single-pass one

line_profiler/autoprofile/_single_pass_transformer.py
    _CompoundNodeChecker
        Refactored internals and the `.from_toggles()` constructor
    _ProfModHelper
        New helper object for dealing with `kernprof`-level tasks having
        to do with the `prof_mod` argument, consisting of code
        refactored from
        `ProfmodExtractor._get_modnames_to_profile_from_prof_mod()` etc.
    SinglePassTransformer
        .__init__()
            - Added new argument `module` for dealing with module and
              non-module rewrite modes (see
              `~.autoprofile.autoprofile.run(as_module=...)`)
            - Extended `config` so that it is possible to pass a
              `ConfigSource` directly
            - Extended `prof_imports_in` to be more flexible with the
              accepted keys
            - Streamlined creation of `._import_ancestry_checker`
            - Added `._dropped_imports` for bookkeeping
        ._visit_import()
            - Added `ast.copy_location()` calls on the created nodes, so
              that if the calls to the profiler's on-import profiling
              pseudo-methods fail, the error locations are attributed
              correctly to the import statement instead of its parent
            - Added bookkeeping for dropped imports
            - Updated usage of `._expl_import_handlers` and
              `._star_import_handlers`
        .visit_ImportFrom()
            Now performing normalization of relative imports, using
            migrated code from `~.autoprofile.run_module`
        ._transform()
            Refactored from `AstProfileTransformer._transform()`
        ._expl_import_handlers, ._star_import_handlers
            Updated signatures to take `ImportTarget`s, fixing the bug
            where node creation for explicit from-imports errors out

line_profiler/autoprofile/ast_profile_transformer.py
::AstProfileTransformer
    ._handle_new_import_target()
        Simplified implementation
    ._get_ast_transformer()
        Refactored from `._transform()`, which used to be a class method
        with a big signature

line_profiler/autoprofile/ast_tree_profiler.py
    __all__
        Added to explicitly delineate the public API
    AstTreeProfiler
        ._check_profile_full_script()
            Refactored to use `_ProfModHelper`
        ._ast_create_node_from_import_target()
            Migrated from
            `~.ast_profile_transformer._ast_create_node_from_import_target()`
        ._profile_ast_tree()
            Updated creation and use of `AstProfileTransformer`

line_profiler/autoprofile/autoprofile.py
    __all__
        Added to explicitly delineate the public API
    run()
        Refactored to use either of two new private functions to
        construct the AST for `script_file`, one using the "legacy"
        system and the other the new single-pass system

line_profiler/autoprofile/profmod_extractor.py
::ProfmodExtractor._modnames_to_profile
    Refactored to use `_ProfModHelper`

line_profiler/autoprofile/run_module.py
    __all__
        Added to explicitly delineate the public API
    ImportFromTransformer.visit_ImportFrom()
        Refactored to use the same private utilities as
        `SinglePassTransformer.visit_ImportFrom()`
    AstTreeModuleProfiler._check_profile_full_script()
        Refactored to use `_ProfModHelper`

tests/test_autoprofile.py
    test_import_discovery_in_all_compound_statements()
    test_nested_imports_correct_deduplication_across_scopes()
    test_ast_profile_transformer_deprecated_profiled_imports()
        Refactored invocations of `AstProfileTransformer._transform()`

TODO:
    The new system doesn't issue warnings about dropped star-imports in
    the exact same cases that the old system would; try to reach parity
    before further refactoring (esp. deprecation/deletion of the old
    system)
line_profiler/autoprofile/_single_pass_transformer.py
    _get_conf_table()
        Wrapper around `ConfigSource.get_subconfig()` taking care of
        cases where `config.subtable != ['tool', 'line_profiler']`,
        which may happen if e.g. a subconfig is passed
    _CompoundNodeChecker._get_toggles()
    SinglePassTransformer.__init__()
        Now using `_get_conf_table()`
line_profiler/autoprofile/autoprofile.py::_rewrite_ast_single_pass()
    Fixed the argument
    `SinglePassTransformer._transform(warn_dropped_star_imports=...)` so
    that warnings against non-profiled star-imports are issued in the
    same cases as when using the old `line_profiler.autoprofile` core
tests/test_autoprofile.py
    test_autoprofile_star_imports()
    test_autoprofile_nested_imports()
    test_multitarget_import_resolution()
    test_nested_import_discovery()
    test_import_discovery_in_all_compound_statements()
    test_nested_imports_correct_deduplication_across_scopes()
        Updated parametrizations and implementations to add subtests for
        the new `line_profiler.autoprofile` core
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant