Skip to content

Conversation

@codeflash-ai
Copy link

@codeflash-ai codeflash-ai bot commented Dec 23, 2025

📄 21,829% (218.29x) speedup for find_last_node in src/algorithms/graph.py

⏱️ Runtime : 77.5 milliseconds 353 microseconds (best of 250 runs)

📝 Explanation and details

The optimized code achieves a 218x speedup by eliminating a quadratic time complexity bottleneck in the original implementation.

Key Optimization

Original approach: For each node, the code checks all(e["source"] != n["id"] for e in edges), which requires scanning through all edges for every node. This results in O(N × E) time complexity where N is the number of nodes and E is the number of edges.

Optimized approach: The code pre-computes a set of all edge sources once (edge_sources = {e["source"] for e in edges}), then performs constant-time O(1) lookups using n["id"] not in edge_sources. This reduces complexity to O(N + E).

Why This Matters

  1. Set lookup vs. repeated iteration: Python sets use hash tables, providing O(1) average-case membership testing. The original code's nested iteration forces O(E) operations per node.

  2. Dramatic impact on larger graphs:

    • With 1000 nodes in a linear chain (999 edges), the original code performs ~999,000 comparisons
    • The optimized version does 999 set insertions + 1000 set lookups (~2,000 operations)
    • This explains the 330x speedup on test_large_linear_chain (18.3ms → 55.2μs)
  3. Consistent improvements: Even small graphs show 50-80% speedups because hash set construction overhead is minimal, and membership testing is immediately faster than repeated iteration.

Test Case Performance Patterns

  • Small graphs (2-3 nodes): 50-86% faster - overhead of set creation is negligible
  • Medium graphs: Speedups scale with edge count
  • Large graphs (1000 nodes): 330x faster for chains/cycles - quadratic overhead elimination is most visible
  • Empty edges: Slightly slower (14-23%) due to unnecessary empty set creation, but this is a trivial edge case

The optimization is universally beneficial for any realistic workload where the function is called with non-trivial graphs, particularly in hot paths where graph analysis is performed repeatedly.

Correctness verification report:

Test Status
⚙️ Existing Unit Tests 🔘 None Found
🌀 Generated Regression Tests 40 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
from src.algorithms.graph import find_last_node

# unit tests

# -----------------------
# Basic Test Cases
# -----------------------


def test_single_node_no_edges():
    # Only one node, no edges. That node is the last node.
    nodes = [{"id": 1}]
    edges = []
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.17μs -> 959ns (21.7% faster)


def test_two_nodes_one_edge():
    # Two nodes, one edge from 1 -> 2. Node 2 is the last node.
    nodes = [{"id": 1}, {"id": 2}]
    edges = [{"source": 1, "target": 2}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.75μs -> 1.12μs (55.6% faster)


def test_three_nodes_linear_chain():
    # 1 -> 2 -> 3, so node 3 is the last node.
    nodes = [{"id": 1}, {"id": 2}, {"id": 3}]
    edges = [{"source": 1, "target": 2}, {"source": 2, "target": 3}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 2.17μs -> 1.25μs (73.3% faster)


def test_multiple_last_nodes_returns_first():
    # 1 -> 2, 1 -> 3, so nodes 2 and 3 are both "last" (no outgoing edges).
    # Should return the first one found (node 2).
    nodes = [{"id": 1}, {"id": 2}, {"id": 3}]
    edges = [{"source": 1, "target": 2}, {"source": 1, "target": 3}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.83μs -> 1.17μs (57.3% faster)


# -----------------------
# Edge Test Cases
# -----------------------


def test_empty_nodes_and_edges():
    # No nodes or edges, should return None
    nodes = []
    edges = []
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 750ns -> 875ns (14.3% slower)


def test_nodes_with_no_edges():
    # Multiple nodes, no edges, should return the first node
    nodes = [{"id": 10}, {"id": 20}, {"id": 30}]
    edges = []
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.17μs -> 958ns (21.8% faster)


def test_cycle_graph():
    # 1 -> 2 -> 3 -> 1 (cycle), so no node is a "last" node
    nodes = [{"id": 1}, {"id": 2}, {"id": 3}]
    edges = [
        {"source": 1, "target": 2},
        {"source": 2, "target": 3},
        {"source": 3, "target": 1},
    ]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 2.21μs -> 1.25μs (76.6% faster)


def test_disconnected_graph():
    # Two disconnected components: 1->2 and 3 (no edges)
    # Node 2 and node 3 are both last nodes, should return node 2 (first found)
    nodes = [{"id": 1}, {"id": 2}, {"id": 3}]
    edges = [{"source": 1, "target": 2}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.79μs -> 1.08μs (65.3% faster)


def test_graph_with_self_loop():
    # Node 1 with a self-loop, node 2 is isolated
    nodes = [{"id": 1}, {"id": 2}]
    edges = [{"source": 1, "target": 1}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.79μs -> 1.12μs (59.2% faster)


def test_graph_with_duplicate_edges():
    # 1 -> 2 (twice), node 2 is still the last node
    nodes = [{"id": 1}, {"id": 2}]
    edges = [{"source": 1, "target": 2}, {"source": 1, "target": 2}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.83μs -> 1.17μs (57.1% faster)


def test_graph_with_nonexistent_edge_sources():
    # Edge references a source not in nodes; should ignore and find last node
    nodes = [{"id": 1}, {"id": 2}]
    edges = [{"source": 3, "target": 1}]  # source 3 does not exist
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.38μs -> 1.08μs (27.0% faster)


def test_graph_with_nonexistent_edge_targets():
    # Edge references a target not in nodes; should not affect last node logic
    nodes = [{"id": 1}, {"id": 2}]
    edges = [{"source": 1, "target": 3}]  # target 3 does not exist
    # Node 2 has no outgoing edges, so should be returned
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.75μs -> 1.12μs (55.6% faster)


def test_nodes_with_non_integer_ids():
    # Node IDs are strings
    nodes = [{"id": "a"}, {"id": "b"}]
    edges = [{"source": "a", "target": "b"}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.83μs -> 1.12μs (63.0% faster)


def test_nodes_with_dict_ids():
    # Node IDs are tuples (hashable)
    nodes = [{"id": (1, 2)}, {"id": (2, 3)}]
    edges = [{"source": (1, 2), "target": (2, 3)}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.92μs -> 1.21μs (58.7% faster)


# -----------------------
# Large Scale Test Cases
# -----------------------


def test_large_linear_chain():
    # Linear chain of 1000 nodes: 0->1->2->...->999, last node is 999
    N = 1000
    nodes = [{"id": i} for i in range(N)]
    edges = [{"source": i, "target": i + 1} for i in range(N - 1)]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 18.2ms -> 55.3μs (32727% faster)


def test_large_star_graph():
    # Star graph: 0 -> all others, so all others are last nodes, should return node 1
    N = 1000
    nodes = [{"id": i} for i in range(N)]
    edges = [{"source": 0, "target": i} for i in range(1, N)]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 37.5μs -> 19.9μs (88.3% faster)


def test_large_graph_no_edges():
    # 1000 nodes, no edges, should return first node
    N = 1000
    nodes = [{"id": i} for i in range(N)]
    edges = []
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.29μs -> 1.04μs (24.0% faster)


def test_large_graph_all_cycles():
    # 1000 nodes, each node i -> (i+1)%N (cycle), so no last node
    N = 1000
    nodes = [{"id": i} for i in range(N)]
    edges = [{"source": i, "target": (i + 1) % N} for i in range(N)]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 18.2ms -> 54.8μs (33121% faster)


def test_large_disconnected_graph():
    # Two large disconnected chains, first chain: 0->1->...->499, second: 500->501->...->999
    N = 1000
    nodes = [{"id": i} for i in range(N)]
    edges = [{"source": i, "target": i + 1} for i in range(0, 499)] + [
        {"source": i, "target": i + 1} for i in range(500, 999)
    ]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 4.54ms -> 38.0μs (11838% faster)


# -----------------------
# Mutation Testing Guards
# -----------------------


def test_mutation_guard_no_false_positives():
    # If the function returns the first node with outgoing edges, this should fail.
    nodes = [{"id": 1}, {"id": 2}, {"id": 3}]
    edges = [{"source": 1, "target": 2}, {"source": 2, "target": 3}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 2.25μs -> 1.21μs (86.3% faster)


def test_mutation_guard_none_for_all_sources():
    # If all nodes have outgoing edges, should return None
    nodes = [{"id": 1}, {"id": 2}]
    edges = [{"source": 1, "target": 2}, {"source": 2, "target": 1}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.83μs -> 1.17μs (57.2% faster)


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

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

# unit tests

# --------------------------
# Basic Test Cases
# --------------------------


def test_single_node_no_edges():
    # Only one node, no edges: should return that node
    nodes = [{"id": 1, "label": "A"}]
    edges = []
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.21μs -> 958ns (26.1% faster)


def test_two_nodes_one_edge():
    # Two nodes, one edge from 1 -> 2: last node is node 2
    nodes = [{"id": 1}, {"id": 2}]
    edges = [{"source": 1, "target": 2}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.71μs -> 1.12μs (51.9% faster)


def test_three_nodes_linear_chain():
    # 1 -> 2 -> 3: last node is node 3
    nodes = [{"id": 1}, {"id": 2}, {"id": 3}]
    edges = [{"source": 1, "target": 2}, {"source": 2, "target": 3}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 2.21μs -> 1.21μs (82.8% faster)


def test_multiple_ends_returns_first_last():
    # 1 -> 2, 1 -> 3: both 2 and 3 are "last" nodes, should return 2 (first found)
    nodes = [{"id": 1}, {"id": 2}, {"id": 3}]
    edges = [{"source": 1, "target": 2}, {"source": 1, "target": 3}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.83μs -> 1.17μs (57.1% faster)


# --------------------------
# Edge Test Cases
# --------------------------


def test_empty_nodes_and_edges():
    # No nodes, no edges: should return None
    nodes = []
    edges = []
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 708ns -> 916ns (22.7% slower)


def test_nodes_no_edges():
    # Multiple nodes, no edges: should return the first node
    nodes = [{"id": "x"}, {"id": "y"}, {"id": "z"}]
    edges = []
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.21μs -> 917ns (31.8% faster)


def test_disconnected_nodes():
    # Nodes not connected by any edge: should return the first node
    nodes = [{"id": "a"}, {"id": "b"}, {"id": "c"}]
    edges = []
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.25μs -> 958ns (30.5% faster)


def test_cycle_graph():
    # 1 -> 2 -> 3 -> 1: cycle, all nodes have outgoing edges, should return None
    nodes = [{"id": 1}, {"id": 2}, {"id": 3}]
    edges = [
        {"source": 1, "target": 2},
        {"source": 2, "target": 3},
        {"source": 3, "target": 1},
    ]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 2.17μs -> 1.25μs (73.4% faster)


def test_self_loop():
    # Node with self-loop: should return None (since it has an outgoing edge)
    nodes = [{"id": "n"}]
    edges = [{"source": "n", "target": "n"}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.46μs -> 1.12μs (29.6% faster)


def test_multiple_last_nodes_with_non_sequential_ids():
    # Graph with multiple last nodes, IDs not ordered
    nodes = [{"id": 10}, {"id": 20}, {"id": 30}]
    edges = [{"source": 10, "target": 20}]
    # Both 20 and 30 are last nodes, should return 20 (first in list)
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.83μs -> 1.17μs (57.1% faster)


def test_edge_with_nonexistent_source():
    # Edge references source not in nodes: should not affect result
    nodes = [{"id": 1}, {"id": 2}]
    edges = [{"source": 3, "target": 1}]  # source 3 not in nodes
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.46μs -> 1.08μs (34.6% faster)


def test_edge_with_nonexistent_target():
    # Edge references target not in nodes: should not affect result
    nodes = [{"id": 1}, {"id": 2}]
    edges = [{"source": 1, "target": 3}]  # target 3 not in nodes
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.75μs -> 1.12μs (55.6% faster)


def test_duplicate_edges():
    # Duplicate edges should not affect result
    nodes = [{"id": 1}, {"id": 2}]
    edges = [{"source": 1, "target": 2}, {"source": 1, "target": 2}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.79μs -> 1.17μs (53.7% faster)


def test_nodes_with_extra_fields():
    # Nodes have extra fields, should still return correct node
    nodes = [{"id": 1, "data": "foo"}, {"id": 2, "data": "bar"}]
    edges = [{"source": 1, "target": 2}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.75μs -> 1.12μs (55.6% faster)


# --------------------------
# Large Scale Test Cases
# --------------------------


def test_large_linear_chain():
    # Large chain of 1000 nodes: 0 -> 1 -> 2 -> ... -> 999
    N = 1000
    nodes = [{"id": i} for i in range(N)]
    edges = [{"source": i, "target": i + 1} for i in range(N - 1)]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 18.3ms -> 55.2μs (32965% faster)


def test_large_star_graph():
    # Star graph: node 0 points to all others, all others are last nodes
    N = 1000
    nodes = [{"id": i} for i in range(N)]
    edges = [{"source": 0, "target": i} for i in range(1, N)]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 38.2μs -> 20.0μs (91.2% faster)


def test_large_disconnected_nodes():
    # 1000 nodes, no edges: should return the first node
    N = 1000
    nodes = [{"id": i} for i in range(N)]
    edges = []
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.25μs -> 1.04μs (20.0% faster)


def test_large_cycle():
    # 1000 nodes in a cycle: no last node, should return None
    N = 1000
    nodes = [{"id": i} for i in range(N)]
    edges = [{"source": i, "target": (i + 1) % N} for i in range(N)]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 18.1ms -> 54.9μs (32964% faster)


def test_large_multiple_last_nodes():
    # 1000 nodes, edges from 0 to 1..999, so nodes 1..999 are all last nodes
    N = 1000
    nodes = [{"id": i} for i in range(N)]
    edges = [{"source": 0, "target": i} for i in range(1, N)]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 37.8μs -> 19.9μs (90.0% 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-mjj6uva3 and push.

Codeflash Static Badge

The optimized code achieves a **218x speedup** by eliminating a quadratic time complexity bottleneck in the original implementation.

## Key Optimization

**Original approach:** For each node, the code checks `all(e["source"] != n["id"] for e in edges)`, which requires scanning through **all edges** for **every node**. This results in O(N × E) time complexity where N is the number of nodes and E is the number of edges.

**Optimized approach:** The code pre-computes a set of all edge sources once (`edge_sources = {e["source"] for e in edges}`), then performs constant-time O(1) lookups using `n["id"] not in edge_sources`. This reduces complexity to O(N + E).

## Why This Matters

1. **Set lookup vs. repeated iteration:** Python sets use hash tables, providing O(1) average-case membership testing. The original code's nested iteration forces O(E) operations per node.

2. **Dramatic impact on larger graphs:** 
   - With 1000 nodes in a linear chain (999 edges), the original code performs ~999,000 comparisons
   - The optimized version does 999 set insertions + 1000 set lookups (~2,000 operations)
   - This explains the **330x speedup** on `test_large_linear_chain` (18.3ms → 55.2μs)

3. **Consistent improvements:** Even small graphs show 50-80% speedups because hash set construction overhead is minimal, and membership testing is immediately faster than repeated iteration.

## Test Case Performance Patterns

- **Small graphs (2-3 nodes):** 50-86% faster - overhead of set creation is negligible
- **Medium graphs:** Speedups scale with edge count
- **Large graphs (1000 nodes):** 330x faster for chains/cycles - quadratic overhead elimination is most visible
- **Empty edges:** Slightly slower (14-23%) due to unnecessary empty set creation, but this is a trivial edge case

The optimization is universally beneficial for any realistic workload where the function is called with non-trivial graphs, particularly in hot paths where graph analysis is performed repeatedly.
@codeflash-ai codeflash-ai bot requested a review from KRRT7 December 23, 2025 23:00
@codeflash-ai codeflash-ai bot added ⚡️ codeflash Optimization PR opened by Codeflash AI 🎯 Quality: High Optimization Quality according to Codeflash labels Dec 23, 2025
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