Skip to content

Comments

⚡️ Speed up function find_last_node by 10,019%#272

Closed
codeflash-ai[bot] wants to merge 1 commit intooptimizefrom
codeflash/optimize-find_last_node-mldvkd8i
Closed

⚡️ Speed up function find_last_node by 10,019%#272
codeflash-ai[bot] wants to merge 1 commit intooptimizefrom
codeflash/optimize-find_last_node-mldvkd8i

Conversation

@codeflash-ai
Copy link

@codeflash-ai codeflash-ai bot commented Feb 8, 2026

📄 10,019% (100.19x) speedup for find_last_node in src/algorithms/graph.py

⏱️ Runtime : 24.8 milliseconds 245 microseconds (best of 250 runs)

📝 Explanation and details

The optimized code achieves a 100x speedup (10018% faster, from 24.8ms to 245μs) by replacing the nested all() comprehension with a set-based membership test.

Key Optimization:

The original code uses:

next((n for n in nodes if all(e["source"] != n["id"] for e in edges)), None)

This creates O(N × M) complexity where N is the number of nodes and M is the number of edges. For each node, it must iterate through ALL edges to verify none match.

The optimized code:

  1. Pre-computes sources into a set: sources = {e["source"] for e in edges}
  2. Uses O(1) membership testing: if n["id"] not in sources

This reduces complexity to O(N + M) - one pass to build the set, one pass to check nodes.

Performance Impact by Test Case:

  • Large-scale tests show dramatic gains: The 500-node chain test improves from 4.85ms to 55.8μs (8581% faster), and the 999-node test from 17.9ms to 48.8μs (36528% faster)
  • Small graphs also benefit: Even simple cases like 3-node chains see 79.9-91.7% improvements
  • Edge case handling: Empty edges are handled efficiently with early return
  • Only regression: Very long node IDs (10k characters) see 67% slowdown due to hashing overhead, but this is an extreme edge case

Why It's Faster:

Set membership (in) uses hash lookups (O(1) average), while the original's all() must scan every edge for every node. With Python's dictionary/set implementation, even small graphs benefit from eliminating redundant iterations. The optimization includes a try/except to preserve generator compatibility while maximizing performance for common list/tuple inputs.

Correctness verification report:

Test Status
⚙️ Existing Unit Tests 🔘 None Found
🌀 Generated Regression Tests 42 Passed
⏪ Replay Tests 🔘 None Found
🔎 Concolic Coverage Tests 🔘 None Found
📊 Tests Coverage 100.0%
🌀 Click to see Generated Regression Tests
from __future__ import annotations

# imports
import pytest  # used for our unit tests
from src.algorithms.graph import find_last_node


def test_empty_nodes_returns_none():
    # When there are no nodes, there is no "last node" to find.
    nodes = []  # empty nodes list
    edges = [{"source": "a"}, {"source": "b"}]  # edges may exist but no nodes to match
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 666ns -> 792ns (15.9% slower)


def test_no_edges_returns_first_node():
    # If there are nodes but no edges, every node has no outgoing edges,
    # therefore the function should return the first node in iteration order.
    nodes = [{"id": "n1"}, {"id": "n2"}]
    edges = []  # no edges at all
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.00μs -> 708ns (41.2% faster)


def test_single_last_node_identified_correctly():
    # A typical flow: nodes 'a','b','c' and edges indicate 'a' and 'b' have outgoing edges.
    # The node 'c' is not a source in any edge, so it should be identified as last.
    nodes = [{"id": "a"}, {"id": "b"}, {"id": "c"}]
    edges = [{"source": "a", "target": "b"}, {"source": "b", "target": "c"}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.88μs -> 1.04μs (79.9% faster)


def test_multiple_candidates_returns_first_candidate():
    # More than one node may have no outgoing edges - function must return the first such node.
    nodes = [{"id": "x"}, {"id": "y"}, {"id": "z"}]
    # Only 'y' is a source; 'x' and 'z' do not appear as sources.
    edges = [{"source": "y", "target": "z"}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.21μs -> 792ns (52.5% faster)


def test_all_nodes_have_outgoing_returns_none():
    # If every node appears as a 'source' in at least one edge, there is no last node.
    nodes = [{"id": 1}, {"id": 2}, {"id": 3}]
    edges = [{"source": 1}, {"source": 2}, {"source": 3}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.92μs -> 1.08μs (77.0% faster)


def test_edges_missing_source_key_raises_keyerror():
    # The implementation indexes edges with e["source"], so a missing 'source' key should raise KeyError.
    nodes = [{"id": "a"}]
    edges = [{"target": "b"}]  # malformed edge without 'source'
    with pytest.raises(KeyError):
        # calling find_last_node should attempt to access e["source"] and raise KeyError
        find_last_node(nodes, edges)  # 1.75μs -> 1.04μs (67.9% faster)


def test_duplicate_ids_prefers_first_instance():
    # When duplicate ids exist in nodes, the search respects nodes iteration order.
    # Here 'a' appears twice; since no edge has 'a' as source, the first 'a' should be returned.
    nodes = [{"id": "a"}, {"id": "b"}, {"id": "a"}]
    edges = [{"source": "b"}]  # only 'b' is a source
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.38μs -> 875ns (57.1% faster)


def test_non_string_and_mixed_id_types_work():
    # The function must correctly compare ids of arbitrary comparable types (e.g., ints).
    nodes = [{"id": 0}, {"id": 1}, {"id": 2}]
    # Use integer sources for edges
    edges = [{"source": 0}, {"source": 1}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.88μs -> 1.04μs (79.9% faster)


def test_node_objects_identity_preserved_in_return():
    # Ensure that the object returned is the same object from the nodes list (not a copy).
    node_a = {"id": "keep_me", "meta": {"x": 1}}
    node_b = {"id": "other"}
    nodes = [node_a, node_b]
    edges = [{"source": "other"}]  # node_b has outgoing edge, node_a does not
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.21μs -> 833ns (45.1% faster)
    # modifying returned object should reflect in original list
    result["meta"]["x"] = 42


def test_large_scale_find_last_node_performance_and_correctness():
    # Create a reasonably large flow (under 1000 elements) to test scalability.
    # We create 999 nodes (ids 0..998) and edges that make every node except the last a source.
    n = 999  # keep under 1000 per instructions
    nodes = [{"id": i} for i in range(n)]  # nodes 0 .. n-1
    # edges: every node except the last is a source
    edges = [{"source": i, "target": i + 1} for i in range(n - 1)]
    # The last node (id n-1) should be the only node that does not appear as a source.
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 17.9ms -> 48.8μs (36528% faster)


def test_order_sensitivity_with_interleaved_candidates():
    # Interleave nodes where some have outgoing edges and some don't,
    # to ensure the algorithm finds the first candidate in order.
    nodes = [
        {"id": "candidate1"},  # should be returned
        {"id": "source1"},
        {"id": "candidate2"},
        {"id": "source2"},
    ]
    edges = [
        {"source": "source1"},
        {"source": "source2"},
    ]  # only source nodes are sources
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.33μs -> 834ns (59.8% faster)


def test_edge_items_with_extra_keys_do_not_affect_result():
    # Extra keys in edge dicts should be ignored; only 'source' matters.
    nodes = [{"id": "a"}, {"id": "b"}]
    edges = [{"source": "a", "weight": 10, "meta": {"x": 1}}]
    # Since 'a' is a source, 'b' should be returned.
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.62μs -> 917ns (77.2% faster)


# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.
import pytest
from src.algorithms.graph import find_last_node


def test_basic_single_node_no_edges():
    """Test with a single node and no edges - node should be returned as last node."""
    nodes = [{"id": "node1", "label": "Start"}]
    edges = []
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.00μs -> 750ns (33.3% faster)


def test_basic_two_nodes_one_edge():
    """Test with two nodes where one has an outgoing edge - the other should be last."""
    nodes = [{"id": "node1", "label": "Start"}, {"id": "node2", "label": "End"}]
    edges = [{"source": "node1", "target": "node2"}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.58μs -> 875ns (81.0% faster)


def test_basic_linear_chain():
    """Test with a linear chain of three nodes - last node in chain should be returned."""
    nodes = [
        {"id": "a", "label": "First"},
        {"id": "b", "label": "Middle"},
        {"id": "c", "label": "Last"},
    ]
    edges = [{"source": "a", "target": "b"}, {"source": "b", "target": "c"}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.92μs -> 1.00μs (91.7% faster)


def test_basic_multiple_edges_from_one_node():
    """Test node with multiple outgoing edges - should not be the last node."""
    nodes = [
        {"id": "start", "label": "Decision"},
        {"id": "yes", "label": "Yes Path"},
        {"id": "no", "label": "No Path"},
    ]
    edges = [{"source": "start", "target": "yes"}, {"source": "start", "target": "no"}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.62μs -> 1.00μs (62.5% faster)


def test_basic_diamond_pattern():
    """Test diamond-shaped flow - bottom node should be last."""
    nodes = [
        {"id": "top", "label": "Top"},
        {"id": "left", "label": "Left"},
        {"id": "right", "label": "Right"},
        {"id": "bottom", "label": "Bottom"},
    ]
    edges = [
        {"source": "top", "target": "left"},
        {"source": "top", "target": "right"},
        {"source": "left", "target": "bottom"},
        {"source": "right", "target": "bottom"},
    ]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 2.42μs -> 1.17μs (107% faster)


def test_edge_empty_nodes_list():
    """Test with empty nodes list - should return None."""
    nodes = []
    edges = []
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 667ns -> 750ns (11.1% slower)


def test_edge_empty_edges_list_multiple_nodes():
    """Test with multiple nodes but no edges - first node should be returned."""
    nodes = [
        {"id": "1", "name": "First"},
        {"id": "2", "name": "Second"},
        {"id": "3", "name": "Third"},
    ]
    edges = []
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.08μs -> 708ns (53.0% faster)


def test_edge_cyclic_graph():
    """Test with cyclic graph - all nodes have outgoing edges, should return None."""
    nodes = [
        {"id": "a", "label": "A"},
        {"id": "b", "label": "B"},
        {"id": "c", "label": "C"},
    ]
    edges = [
        {"source": "a", "target": "b"},
        {"source": "b", "target": "c"},
        {"source": "c", "target": "a"},  # Creates cycle
    ]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 2.04μs -> 1.08μs (88.3% faster)


def test_edge_self_loop():
    """Test with node that has a self-loop edge."""
    nodes = [{"id": "loop", "label": "Loop Node"}, {"id": "end", "label": "End Node"}]
    edges = [{"source": "loop", "target": "loop"}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.54μs -> 917ns (68.0% faster)


def test_edge_node_id_as_integer():
    """Test with numeric node IDs instead of strings."""
    nodes = [{"id": 1, "label": "Node 1"}, {"id": 2, "label": "Node 2"}]
    edges = [{"source": 1, "target": 2}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.54μs -> 959ns (60.8% faster)


def test_edge_edge_with_extra_fields():
    """Test edges with additional metadata fields - should not affect logic."""
    nodes = [{"id": "start", "type": "begin"}, {"id": "end", "type": "finish"}]
    edges = [
        {
            "source": "start",
            "target": "end",
            "label": "goes to",
            "weight": 1,
            "color": "red",
        }
    ]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.54μs -> 916ns (68.3% faster)


def test_edge_node_without_label():
    """Test nodes that only have an id field."""
    nodes = [{"id": "x"}, {"id": "y"}]
    edges = [{"source": "x", "target": "y"}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.50μs -> 875ns (71.4% faster)


def test_edge_completely_disconnected_nodes():
    """Test with multiple disconnected components - first node with no outgoing edge returned."""
    nodes = [
        {"id": "a", "label": "Component 1"},
        {"id": "b", "label": "Component 1"},
        {"id": "c", "label": "Component 2"},
        {"id": "d", "label": "Component 2"},
    ]
    edges = [{"source": "a", "target": "b"}, {"source": "c", "target": "d"}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.67μs -> 1.00μs (66.6% faster)


def test_edge_only_isolated_nodes():
    """Test with all nodes being isolated (no edges at all)."""
    nodes = [{"id": "isolated1"}, {"id": "isolated2"}, {"id": "isolated3"}]
    edges = []
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.04μs -> 708ns (47.2% faster)


def test_edge_very_long_node_id():
    """Test with very long node ID strings."""
    long_id = "a" * 10000
    nodes = [
        {"id": long_id, "label": "Long ID Node"},
        {"id": "normal", "label": "Normal Node"},
    ]
    edges = [{"source": long_id, "target": "normal"}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.67μs -> 5.08μs (67.2% slower)


def test_edge_special_characters_in_id():
    """Test with special characters in node IDs."""
    nodes = [
        {"id": "node@#$%^&*()", "label": "Special"},
        {"id": "node-with-dash", "label": "Normal"},
    ]
    edges = [{"source": "node@#$%^&*()", "target": "node-with-dash"}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.62μs -> 917ns (77.2% faster)


def test_edge_unicode_characters_in_id():
    """Test with unicode characters in node IDs."""
    nodes = [
        {"id": "节点1", "label": "Chinese Node"},
        {"id": "узел2", "label": "Russian Node"},
    ]
    edges = [{"source": "节点1", "target": "узел2"}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.58μs -> 916ns (72.8% faster)


def test_edge_multiple_edges_to_same_target():
    """Test with multiple edges pointing to the same target node."""
    nodes = [{"id": "a"}, {"id": "b"}, {"id": "c"}, {"id": "d"}]
    edges = [
        {"source": "a", "target": "d"},
        {"source": "b", "target": "d"},
        {"source": "c", "target": "d"},
    ]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 2.29μs -> 1.12μs (104% faster)


def test_edge_node_with_id_matching_literal():
    """Test with node IDs that could be problematic (None, False, 0, empty string)."""
    nodes = [
        {"id": 0, "label": "Zero"},
        {"id": None, "label": "None Value"},
        {"id": "", "label": "Empty String"},
    ]
    edges = [{"source": 0, "target": None}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.71μs -> 1.00μs (70.8% faster)


def test_large_scale_linear_chain_100_nodes():
    """Test with a linear chain of 100 nodes."""
    nodes = [{"id": f"node_{i}", "index": i} for i in range(100)]
    edges = [{"source": f"node_{i}", "target": f"node_{i+1}"} for i in range(99)]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 222μs -> 13.2μs (1583% faster)


def test_large_scale_binary_tree_structure():
    """Test with a binary tree structure of multiple levels."""
    nodes = []
    edges = []
    # Create nodes for a binary tree with 5 levels (31 nodes)
    node_id = 0
    for i in range(31):
        nodes.append({"id": i, "level": i // 15})

    # Create edges for binary tree structure
    for i in range(15):
        left_child = 2 * i + 1
        right_child = 2 * i + 2
        if left_child < 31:
            edges.append({"source": i, "target": left_child})
        if right_child < 31:
            edges.append({"source": i, "target": right_child})

    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 13.8μs -> 2.25μs (515% faster)


def test_large_scale_multiple_endpoints():
    """Test with a graph having multiple endpoint nodes (no outgoing edges)."""
    nodes = [{"id": f"n_{i}"} for i in range(100)]
    edges = []
    # Create edges from first 50 nodes to last 50 nodes (many-to-many)
    for i in range(50):
        for j in range(50, 75):
            edges.append({"source": f"n_{i}", "target": f"n_{j}"})

    # Nodes 75-99 have no outgoing edges
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.26ms -> 45.7μs (2670% faster)


def test_large_scale_star_topology():
    """Test with star topology - one central node connected to many others."""
    central = {"id": "center", "role": "hub"}
    nodes = [central]
    edges = []

    # Create 200 peripheral nodes and connect them to center
    for i in range(200):
        nodes.append({"id": f"peripheral_{i}", "index": i})
        edges.append({"source": "center", "target": f"peripheral_{i}"})

    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 9.42μs -> 4.88μs (93.1% faster)


def test_large_scale_dense_graph():
    """Test with a more densely connected graph."""
    n = 50
    nodes = [{"id": i} for i in range(n)]
    edges = []

    # Create edges in a pattern where earlier nodes connect to later ones
    for i in range(n):
        for j in range(i + 1, min(i + 5, n)):
            edges.append({"source": i, "target": j})

    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 188μs -> 6.83μs (2654% faster)


def test_large_scale_many_isolated_nodes_with_chain():
    """Test with mostly isolated nodes plus one chain."""
    nodes = [{"id": f"isolated_{i}"} for i in range(200)]
    # Add a chain at the end
    nodes.extend([{"id": f"chain_{i}"} for i in range(10)])

    edges = []
    # Create chain
    for i in range(9):
        edges.append({"source": f"chain_{i}", "target": f"chain_{i+1}"})

    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.71μs -> 1.46μs (17.1% faster)


def test_large_scale_complex_multilevel_hierarchy():
    """Test with a complex multi-level hierarchy (500 nodes)."""
    nodes = []
    edges = []
    nodes_per_level = 10
    num_levels = 5

    # Create hierarchical levels
    for level in range(num_levels):
        for idx in range(nodes_per_level):
            node_id = level * nodes_per_level + idx
            nodes.append({"id": node_id, "level": level})

    # Connect each level to the next
    for level in range(num_levels - 1):
        for idx in range(nodes_per_level):
            source_id = level * nodes_per_level + idx
            # Each source connects to next level
            target_id = (level + 1) * nodes_per_level + (idx % nodes_per_level)
            edges.append({"source": source_id, "target": target_id})

    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 40.0μs -> 3.67μs (991% faster)


def test_large_scale_wide_graph():
    """Test with very wide graph (one level with many nodes)."""
    # Create 100 root nodes and 100 leaf nodes
    nodes = [{"id": f"root_{i}"} for i in range(100)]
    nodes.extend([{"id": f"leaf_{i}"} for i in range(100)])

    edges = []
    # Each root connects to corresponding leaf
    for i in range(100):
        edges.append({"source": f"root_{i}", "target": f"leaf_{i}"})

    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 226μs -> 13.2μs (1613% faster)


def test_large_scale_performance_with_many_edges_per_node():
    """Test performance when nodes have many outgoing edges."""
    nodes = [{"id": i} for i in range(100)]
    edges = []

    # Node 0 connects to all other nodes
    for i in range(1, 100):
        edges.append({"source": 0, "target": i})

    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 5.67μs -> 2.92μs (94.3% faster)


def test_large_scale_deeply_nested_chain_500_nodes():
    """Test with a very long sequential chain of 500 nodes."""
    nodes = [{"id": f"step_{i}"} for i in range(500)]
    edges = [{"source": f"step_{i}", "target": f"step_{i+1}"} for i in range(499)]

    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 4.85ms -> 55.8μs (8581% faster)


def test_large_scale_multiple_independent_chains():
    """Test with 10 independent chains of 50 nodes each."""
    nodes = []
    edges = []

    for chain in range(10):
        for step in range(50):
            node_id = chain * 50 + step
            nodes.append({"id": node_id, "chain": chain})

        # Connect nodes within chain
        for step in range(49):
            source = chain * 50 + step
            target = chain * 50 + step + 1
            edges.append({"source": source, "target": target})

    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 75.0μs -> 14.8μs (407% faster)


# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.

To edit these changes git checkout codeflash/optimize-find_last_node-mldvkd8i and push.

Codeflash Static Badge

The optimized code achieves a **100x speedup** (10018% faster, from 24.8ms to 245μs) by replacing the nested `all()` comprehension with a set-based membership test.

**Key Optimization:**

The original code uses:
```python
next((n for n in nodes if all(e["source"] != n["id"] for e in edges)), None)
```

This creates **O(N × M)** complexity where N is the number of nodes and M is the number of edges. For each node, it must iterate through ALL edges to verify none match.

The optimized code:
1. **Pre-computes sources** into a set: `sources = {e["source"] for e in edges}`
2. **Uses O(1) membership testing**: `if n["id"] not in sources`

This reduces complexity to **O(N + M)** - one pass to build the set, one pass to check nodes.

**Performance Impact by Test Case:**

- **Large-scale tests show dramatic gains**: The 500-node chain test improves from 4.85ms to 55.8μs (8581% faster), and the 999-node test from 17.9ms to 48.8μs (36528% faster)
- **Small graphs also benefit**: Even simple cases like 3-node chains see 79.9-91.7% improvements
- **Edge case handling**: Empty edges are handled efficiently with early return
- **Only regression**: Very long node IDs (10k characters) see 67% slowdown due to hashing overhead, but this is an extreme edge case

**Why It's Faster:**

Set membership (`in`) uses hash lookups (O(1) average), while the original's `all()` must scan every edge for every node. With Python's dictionary/set implementation, even small graphs benefit from eliminating redundant iterations. The optimization includes a try/except to preserve generator compatibility while maximizing performance for common list/tuple inputs.
@codeflash-ai codeflash-ai bot requested a review from KRRT7 February 8, 2026 15:05
@codeflash-ai codeflash-ai bot added ⚡️ codeflash Optimization PR opened by Codeflash AI 🎯 Quality: High Optimization Quality according to Codeflash labels Feb 8, 2026
@KRRT7 KRRT7 closed this Feb 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚡️ codeflash Optimization PR opened by Codeflash AI 🎯 Quality: High Optimization Quality according to Codeflash

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant