import asyncio
import gc
import tracemalloc
from dataclasses import dataclass, field
from os import getenv
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union
from uuid import uuid4

from agno.db.base import AsyncBaseDb, BaseDb
from agno.db.schemas.evals import EvalType
from agno.eval.utils import async_log_eval, log_eval_run, store_result_in_file
from agno.utils.log import log_debug, set_log_level_to_debug, set_log_level_to_info
from agno.utils.timer import Timer

if TYPE_CHECKING:
    from rich.console import Console


@dataclass
class PerformanceResult:
    """
    Holds run-time and memory-usage statistics.
    In addition to average, min, max, std dev, we add median and percentile metrics
    for a more robust understanding.
    """

    # Run time performance in seconds
    run_times: List[float] = field(default_factory=list)
    avg_run_time: float = field(init=False)
    min_run_time: float = field(init=False)
    max_run_time: float = field(init=False)
    std_dev_run_time: float = field(init=False)
    median_run_time: float = field(init=False)
    p95_run_time: float = field(init=False)

    # Memory performance in MiB
    memory_usages: List[float] = field(default_factory=list)
    avg_memory_usage: float = field(init=False)
    min_memory_usage: float = field(init=False)
    max_memory_usage: float = field(init=False)
    std_dev_memory_usage: float = field(init=False)
    median_memory_usage: float = field(init=False)
    p95_memory_usage: float = field(init=False)

    def __post_init__(self):
        self.compute_stats()

    def compute_stats(self):
        """Compute a variety of statistics for both runtime and memory usage."""
        import statistics

        def safe_stats(data: List[float]):
            """Compute stats for a non-empty list of floats."""
            data_sorted = sorted(data)  # ensure data is sorted for correct percentile
            avg = statistics.mean(data_sorted)
            mn = data_sorted[0]
            mx = data_sorted[-1]
            std = statistics.stdev(data_sorted) if len(data_sorted) > 1 else 0
            med = statistics.median(data_sorted)
            # For 95th percentile, use statistics.quantiles
            p95 = statistics.quantiles(data_sorted, n=100)[94] if len(data_sorted) > 1 else 0
            return avg, mn, mx, std, med, p95

        # Populate runtime stats
        if self.run_times:
            (
                self.avg_run_time,
                self.min_run_time,
                self.max_run_time,
                self.std_dev_run_time,
                self.median_run_time,
                self.p95_run_time,
            ) = safe_stats(self.run_times)
        else:
            self.avg_run_time = 0
            self.min_run_time = 0
            self.max_run_time = 0
            self.std_dev_run_time = 0
            self.median_run_time = 0
            self.p95_run_time = 0

        # Populate memory stats
        if self.memory_usages:
            (
                self.avg_memory_usage,
                self.min_memory_usage,
                self.max_memory_usage,
                self.std_dev_memory_usage,
                self.median_memory_usage,
                self.p95_memory_usage,
            ) = safe_stats(self.memory_usages)
        else:
            self.avg_memory_usage = 0
            self.min_memory_usage = 0
            self.max_memory_usage = 0
            self.std_dev_memory_usage = 0
            self.median_memory_usage = 0
            self.p95_memory_usage = 0

    def print_summary(
        self, console: Optional["Console"] = None, measure_memory: bool = True, measure_runtime: bool = True
    ):
        """
        Prints a summary table of the computed stats.
        """
        from rich.console import Console
        from rich.table import Table

        if console is None:
            console = Console()

        # Create performance table
        perf_table = Table(title="Performance Summary", show_header=True, header_style="bold magenta")
        perf_table.add_column("Metric", style="cyan")
        if measure_runtime:
            perf_table.add_column("Time (seconds)", style="green")
        if measure_memory:
            perf_table.add_column("Memory (MiB)", style="yellow")

        # Add rows
        if measure_runtime and measure_memory:
            perf_table.add_row("Average", f"{self.avg_run_time:.6f}", f"{self.avg_memory_usage:.6f}")
            perf_table.add_row("Minimum", f"{self.min_run_time:.6f}", f"{self.min_memory_usage:.6f}")
            perf_table.add_row("Maximum", f"{self.max_run_time:.6f}", f"{self.max_memory_usage:.6f}")
            perf_table.add_row("Std Dev", f"{self.std_dev_run_time:.6f}", f"{self.std_dev_memory_usage:.6f}")
            perf_table.add_row("Median", f"{self.median_run_time:.6f}", f"{self.median_memory_usage:.6f}")
            perf_table.add_row("95th %ile", f"{self.p95_run_time:.6f}", f"{self.p95_memory_usage:.6f}")
        elif measure_runtime:
            perf_table.add_row("Average", f"{self.avg_run_time:.6f}")
            perf_table.add_row("Minimum", f"{self.min_run_time:.6f}")
            perf_table.add_row("Maximum", f"{self.max_run_time:.6f}")
            perf_table.add_row("Std Dev", f"{self.std_dev_run_time:.6f}")
            perf_table.add_row("Median", f"{self.median_run_time:.6f}")
            perf_table.add_row("95th %ile", f"{self.p95_run_time:.6f}")
        elif measure_memory:
            perf_table.add_row("Average", f"{self.avg_memory_usage:.6f}")
            perf_table.add_row("Minimum", f"{self.min_memory_usage:.6f}")
            perf_table.add_row("Maximum", f"{self.max_memory_usage:.6f}")
            perf_table.add_row("Std Dev", f"{self.std_dev_memory_usage:.6f}")
            perf_table.add_row("Median", f"{self.median_memory_usage:.6f}")
            perf_table.add_row("95th %ile", f"{self.p95_memory_usage:.6f}")

        console.print(perf_table)

    def print_results(
        self, console: Optional["Console"] = None, measure_memory: bool = True, measure_runtime: bool = True
    ):
        """
        Prints individual run results in tabular form.
        """
        from rich.console import Console
        from rich.table import Table

        if console is None:
            console = Console()

        # Create runs table
        results_table = Table(title="Individual Runs", show_header=True, header_style="bold magenta")
        results_table.add_column("Run #", style="cyan")
        if measure_runtime:
            results_table.add_column("Time (seconds)", style="green")
        if measure_memory:
            results_table.add_column("Memory (MiB)", style="yellow")

        # Add rows
        for i in range(len(self.run_times) or len(self.memory_usages)):
            if measure_runtime and measure_memory:
                results_table.add_row(str(i + 1), f"{self.run_times[i]:.6f}", f"{self.memory_usages[i]:.6f}")
            elif measure_runtime:
                results_table.add_row(str(i + 1), f"{self.run_times[i]:.6f}")
            elif measure_memory:
                results_table.add_row(str(i + 1), f"{self.memory_usages[i]:.6f}")
            else:
                results_table.add_row(str(i + 1))

        console.print(results_table)


@dataclass
class PerformanceEval:
    """
    Evaluate the performance of a function by measuring run time and peak memory usage.

    - Warm-up runs are included to avoid measuring overhead on the first execution(s).
    - Debug mode can show top memory allocations using tracemalloc snapshots.
    - Optionally, you can enable cProfile for CPU profiling stats.
    """

    # Function to evaluate
    func: Callable
    measure_runtime: bool = True
    measure_memory: bool = True

    # Evaluation name
    name: Optional[str] = None
    # Evaluation UUID
    eval_id: str = field(default_factory=lambda: str(uuid4()))
    # Number of warm-up runs (not included in final stats)
    warmup_runs: Optional[int] = 10
    # Number of measured iterations
    num_iterations: int = 50
    # Result of the evaluation
    result: Optional[PerformanceResult] = None

    # Print summary of results
    print_summary: bool = False
    # Print detailed results
    print_results: bool = False
    # Print detailed memory growth analysis
    memory_growth_tracking: bool = False
    # Number of memory allocations to track
    top_n_memory_allocations: int = 5

    # Agent and Team information
    agent_id: Optional[str] = None
    team_id: Optional[str] = None
    model_id: Optional[str] = None
    model_provider: Optional[str] = None

    # If set, results will be saved in the given file path
    file_path_to_save_results: Optional[str] = None
    # Enable debug logs
    debug_mode: bool = getenv("AGNO_DEBUG", "false").lower() == "true"
    # The database to store Evaluation results
    db: Optional[Union[BaseDb, AsyncBaseDb]] = None

    # Telemetry settings
    # telemetry=True logs minimal telemetry for analytics
    # This helps us improve our Evals and provide better support
    telemetry: bool = True

    def _measure_time(self) -> float:
        """Measure execution time for a single run."""
        timer = Timer()

        timer.start()
        self.func()
        timer.stop()
        self._set_log_level()  # Set log level incase function changed it

        return timer.elapsed

    async def _async_measure_time(self) -> float:
        """Measure execution time for a single run of the contextual async function."""
        timer = Timer()

        timer.start()
        await self.func()
        timer.stop()
        self._set_log_level()  # Set log level incase function changed it

        return timer.elapsed

    def _measure_memory(self, baseline: float) -> float:
        """
        Measures peak memory usage using tracemalloc.
        Subtracts the provided 'baseline' to compute an adjusted usage.
        """

        # Clear memory before measurement
        gc.collect()
        # Start tracing memory
        tracemalloc.start()

        self.func()

        # Get peak memory usage
        current, peak = tracemalloc.get_traced_memory()

        # Stop tracing memory
        tracemalloc.stop()

        self._set_log_level()  # Set log level incase function changed it

        # Convert to MiB and subtract baseline
        peak_mib = peak / 1024 / 1024
        adjusted_usage = max(0, peak_mib - baseline)

        if self.debug_mode:
            log_debug(f"[DEBUG] Raw peak usage: {peak_mib:.6f} MiB, Adjusted: {adjusted_usage:.6f} MiB")

        return adjusted_usage

    def _parse_eval_run_data(self) -> dict:
        """Parse the evaluation result into a dictionary with the data we want for monitoring."""
        if self.result is None:
            return {}

        return {
            "result": {
                "avg_run_time": self.result.avg_run_time,
                "min_run_time": self.result.min_run_time,
                "max_run_time": self.result.max_run_time,
                "std_dev_run_time": self.result.std_dev_run_time,
                "median_run_time": self.result.median_run_time,
                "p95_run_time": self.result.p95_run_time,
                "avg_memory_usage": self.result.avg_memory_usage,
                "min_memory_usage": self.result.min_memory_usage,
                "max_memory_usage": self.result.max_memory_usage,
                "std_dev_memory_usage": self.result.std_dev_memory_usage,
                "median_memory_usage": self.result.median_memory_usage,
                "p95_memory_usage": self.result.p95_memory_usage,
            },
            "runs": [
                {"runtime": runtime, "memory": memory_usage}
                for runtime, memory_usage in zip(self.result.run_times, self.result.memory_usages)
            ],
        }

    async def _async_measure_memory(self, baseline: float) -> float:
        """
        Measures peak memory usage using tracemalloc for async functions.
        Subtracts the provided 'baseline' to compute an adjusted usage.
        """

        # Clear memory before measurement
        gc.collect()
        # Start tracing memory
        tracemalloc.start()

        await self.func()

        # Get peak memory usage
        current, peak = tracemalloc.get_traced_memory()

        # Stop tracing memory
        tracemalloc.stop()
        self._set_log_level()  # Set log level incase function changed it

        # Convert to MiB and subtract baseline
        peak_mib = peak / 1024 / 1024
        adjusted_usage = max(0, peak_mib - baseline)

        if self.debug_mode:
            log_debug(f"[DEBUG] Raw peak usage: {peak_mib:.6f} MiB, Adjusted: {adjusted_usage:.6f} MiB")

        return adjusted_usage

    def _compute_tracemalloc_baseline(self, samples: int = 3) -> float:
        """
        Runs tracemalloc multiple times with an empty function to establish
        a stable average baseline for memory usage in MiB.
        """

        def empty_func():
            return

        results = []
        for _ in range(samples):
            gc.collect()
            tracemalloc.start()
            empty_func()
            _, peak = tracemalloc.get_traced_memory()
            tracemalloc.stop()
            results.append(peak / 1024 / 1024)

        return sum(results) / len(results) if results else 0

    def _set_log_level(self):
        if self.debug_mode:
            set_log_level_to_debug()
        else:
            set_log_level_to_info()

    def _compare_memory_snapshots(self, snapshot1, snapshot2, top_n: int):
        """
        Compare two memory snapshots to identify what's causing memory growth.
        """
        if self.debug_mode:
            log_debug("[DEBUG] Memory growth analysis:")

            # Compare snapshots to find new allocations
            stats = snapshot2.compare_to(snapshot1, "lineno")

            log_debug(f"[DEBUG] Top {top_n} memory growth sources:")
            growth_found = False
            for stat in stats[:top_n]:
                if stat.size_diff > 0:  # Only show growth
                    growth_found = True
                    log_debug(f"  +{stat.size_diff / 1024 / 1024:.1f} MiB: {stat.count_diff} new blocks")
                    log_debug(f"    {stat.traceback.format()}")

            if not growth_found:
                log_debug("  No significant memory growth detected between snapshots")

            # Show total growth
            total_growth = sum(stat.size_diff for stat in stats if stat.size_diff > 0)
            total_shrinkage = sum(abs(stat.size_diff) for stat in stats if stat.size_diff < 0)
            log_debug(f"[DEBUG] Total memory growth: {total_growth / 1024 / 1024:.1f} MiB")
            log_debug(f"[DEBUG] Total memory freed: {total_shrinkage / 1024 / 1024:.1f} MiB")
            log_debug(f"[DEBUG] Net memory change: {(total_growth - total_shrinkage) / 1024 / 1024:.1f} MiB")

    def _measure_memory_with_growth_tracking(
        self, baseline: float, previous_snapshot=None
    ) -> tuple[float, tracemalloc.Snapshot]:
        """
        Enhanced memory measurement that tracks growth between runs.
        Returns (adjusted_usage, current_snapshot)
        """
        # Clear memory before measurement
        gc.collect()
        # Start tracing memory
        tracemalloc.start()

        self.func()

        # Get peak memory usage
        current, peak = tracemalloc.get_traced_memory()
        # Take snapshot before stopping
        current_snapshot = tracemalloc.take_snapshot()
        # Stop tracing memory
        tracemalloc.stop()

        self._set_log_level()  # Set log level incase function changed it

        # Convert to MiB and subtract baseline
        peak_mib = peak / 1024 / 1024
        adjusted_usage = max(0, peak_mib - baseline)

        if self.debug_mode and current_snapshot:
            log_debug(f"[DEBUG] Raw peak usage: {peak_mib:.6f} MiB, Adjusted: {adjusted_usage:.6f} MiB")

            # Compare with previous snapshot if available
            if previous_snapshot is not None:
                self._compare_memory_snapshots(previous_snapshot, current_snapshot, self.top_n_memory_allocations)
            else:
                # Get detailed memory allocation statistics
                top_stats = current_snapshot.statistics("lineno")

                log_debug(f"[DEBUG] Top {self.top_n_memory_allocations} memory allocations:")
                for stat in top_stats[: self.top_n_memory_allocations]:
                    log_debug(f"  {stat.count} blocks: {stat.size / 1024 / 1024:.1f} MiB")
                    log_debug(f"    {stat.traceback.format()}")

        return adjusted_usage, current_snapshot

    async def _async_measure_memory_with_growth_tracking(
        self, baseline: float, previous_snapshot=None
    ) -> tuple[float, tracemalloc.Snapshot]:
        """
        Enhanced async memory measurement that tracks growth between runs.
        Returns (adjusted_usage, current_snapshot)
        """
        # Clear memory before measurement
        gc.collect()
        # Start tracing memory
        tracemalloc.start()

        await self.func()

        # Get peak memory usage
        current, peak = tracemalloc.get_traced_memory()
        # Take snapshot before stopping
        current_snapshot = tracemalloc.take_snapshot()
        # Stop tracing memory
        tracemalloc.stop()

        self._set_log_level()  # Set log level incase function changed it

        # Convert to MiB and subtract baseline
        peak_mib = peak / 1024 / 1024
        adjusted_usage = max(0, peak_mib - baseline)

        if self.debug_mode and current_snapshot:
            log_debug(f"[DEBUG] Raw peak usage: {peak_mib:.6f} MiB, Adjusted: {adjusted_usage:.6f} MiB")

            # Compare with previous snapshot if available
            if previous_snapshot is not None:
                self._compare_memory_snapshots(previous_snapshot, current_snapshot, self.top_n_memory_allocations)
            else:
                # Get detailed memory allocation statistics
                top_stats = current_snapshot.statistics("lineno")

                log_debug(f"[DEBUG] Top {self.top_n_memory_allocations} memory allocations:")
                for stat in top_stats[: self.top_n_memory_allocations]:
                    log_debug(f"  {stat.count} blocks: {stat.size / 1024 / 1024:.1f} MiB")
                    log_debug(f"    {stat.traceback.format()}")

        return adjusted_usage, current_snapshot

    def run(
        self, *, print_summary: bool = False, print_results: bool = False, memory_growth_tracking: bool = False
    ) -> PerformanceResult:
        """
        Main method to run the performance evaluation.
        1. Do optional warm-up runs.
        2. Measure runtime
        3. Measure memory
        4. Collect results
        5. Save results if requested
        6. Print results as requested
        7. Log results to the Agno platform if requested
        """
        if isinstance(self.db, AsyncBaseDb):
            raise ValueError("run() is not supported with an async DB. Please use arun() instead.")

        from rich.console import Console
        from rich.live import Live
        from rich.status import Status

        run_times = []
        memory_usages = []
        previous_snapshot = None

        self._set_log_level()

        log_debug(f"************ Evaluation Start: {self.eval_id} ************")

        # Add a spinner while running the evaluations
        console = Console()
        with Live(console=console, transient=True) as live_log:
            # 1. Do optional warm-up runs.
            if self.warmup_runs is not None:
                for i in range(self.warmup_runs):
                    status = Status(f"Warm-up run {i + 1}/{self.warmup_runs}...", spinner="dots", speed=1.0)
                    live_log.update(status)
                    self.func()  # Simply run the function without measuring
                    self._set_log_level()  # Set log level incase function changed it
                    status.stop()

            # 2. Measure runtime
            if self.measure_runtime:
                for i in range(self.num_iterations):
                    status = Status(
                        f"Runtime measurement {i + 1}/{self.num_iterations}...",
                        spinner="dots",
                        speed=1.0,
                        refresh_per_second=10,
                    )
                    live_log.update(status)
                    # Measure runtime
                    elapsed_time = self._measure_time()
                    run_times.append(elapsed_time)

                    log_debug(f"Run {i + 1} - Time taken: {elapsed_time:.6f} seconds")
                    status.stop()

            # 3. Measure memory
            if self.measure_memory:
                # 3.1 Compute memory baseline
                memory_baseline = 0.0
                memory_baseline = self._compute_tracemalloc_baseline()
                log_debug(f"Computed memory baseline: {memory_baseline:.6f} MiB")

                for i in range(self.num_iterations):
                    status = Status(
                        f"Memory measurement {i + 1}/{self.num_iterations}...",
                        spinner="dots",
                        speed=1.0,
                        refresh_per_second=10,
                    )
                    live_log.update(status)

                    # Measure memory
                    if self.memory_growth_tracking or memory_growth_tracking:
                        usage, current_snapshot = self._measure_memory_with_growth_tracking(
                            memory_baseline, previous_snapshot
                        )

                        # Update previous snapshot for next iteration
                        previous_snapshot = current_snapshot

                    else:
                        usage = self._measure_memory(memory_baseline)
                    memory_usages.append(usage)
                    log_debug(f"Run {i + 1} - Memory usage: {usage:.6f} MiB (adjusted)")

                    status.stop()

        # 4. Collect results
        self.result = PerformanceResult(run_times=run_times, memory_usages=memory_usages)

        # 5. Save result to file if requested
        if self.file_path_to_save_results is not None and self.result is not None:
            store_result_in_file(
                file_path=self.file_path_to_save_results,
                name=self.name,
                eval_id=self.eval_id,
                result=self.result,
            )

        # 6. Print results if requested
        if self.print_results or print_results:
            self.result.print_results(console, measure_memory=self.measure_memory, measure_runtime=self.measure_runtime)
        if self.print_summary or print_summary:
            self.result.print_summary(console, measure_memory=self.measure_memory, measure_runtime=self.measure_runtime)

        # 7. Log results to the Agno platform if requested
        if self.db:
            eval_input = {
                "num_iterations": self.num_iterations,
                "warmup_runs": self.warmup_runs,
            }

            log_eval_run(
                db=self.db,
                run_id=self.eval_id,  # type: ignore
                run_data=self._parse_eval_run_data(),
                eval_type=EvalType.PERFORMANCE,
                name=self.name if self.name is not None else None,
                evaluated_component_name=self.func.__name__,
                agent_id=self.agent_id,
                team_id=self.team_id,
                model_id=self.model_id,
                model_provider=self.model_provider,
                eval_input=eval_input,
            )

        if self.telemetry:
            from agno.api.evals import EvalRunCreate, create_eval_run_telemetry

            create_eval_run_telemetry(
                eval_run=EvalRunCreate(
                    run_id=self.eval_id, eval_type=EvalType.PERFORMANCE, data=self._get_telemetry_data()
                ),
            )

        log_debug(f"*********** Evaluation End: {self.eval_id} ***********")
        return self.result

    async def arun(
        self, *, print_summary: bool = False, print_results: bool = False, memory_growth_tracking: bool = False
    ) -> PerformanceResult:
        """
        Async method to run the performance evaluation of async functions.
        1. Do optional warm-up runs.
        2. Measure runtime
        3. Measure memory
        4. Collect results
        5. Save results if requested
        6. Print results as requested
        7. Log results to the Agno platform if requested
        """
        # Validate the function to evaluate is async.
        if not asyncio.iscoroutinefunction(self.func):
            raise ValueError(
                f"The provided function ({self.func.__name__}) is not async. Use the run() method for sync functions."
            )

        from rich.console import Console
        from rich.live import Live
        from rich.status import Status

        run_times = []
        memory_usages = []
        previous_snapshot = None

        self._set_log_level()

        log_debug(f"************ Evaluation Start: {self.eval_id} ************")

        # Add a spinner while running the evaluations
        console = Console()
        with Live(console=console, transient=True) as live_log:
            # 1. Do optional warm-up runs.
            if self.warmup_runs is not None:
                for i in range(self.warmup_runs):
                    status = Status(f"Warm-up run {i + 1}/{self.warmup_runs}...", spinner="dots", speed=1.0)
                    live_log.update(status)
                    await self.func()  # Simply run the function without measuring
                    self._set_log_level()  # Set log level incase function changed it
                    status.stop()

            # 2. Measure runtime
            if self.measure_runtime:
                for i in range(self.num_iterations):
                    status = Status(
                        f"Runtime measurement {i + 1}/{self.num_iterations}...",
                        spinner="dots",
                        speed=1.0,
                        refresh_per_second=10,
                    )
                    live_log.update(status)
                    # Measure runtime
                    elapsed_time = await self._async_measure_time()
                    run_times.append(elapsed_time)

                    log_debug(f"Run {i + 1} - Time taken: {elapsed_time:.6f} seconds")
                    status.stop()

            # 3. Measure memory
            if self.measure_memory:
                # 3.1 Compute memory baseline
                memory_baseline = 0.0
                memory_baseline = self._compute_tracemalloc_baseline()
                log_debug(f"Computed memory baseline: {memory_baseline:.6f} MiB")

                for i in range(self.num_iterations):
                    status = Status(
                        f"Memory measurement {i + 1}/{self.num_iterations}...",
                        spinner="dots",
                        speed=1.0,
                        refresh_per_second=10,
                    )
                    live_log.update(status)

                    # Measure memory
                    if self.memory_growth_tracking or memory_growth_tracking:
                        usage, current_snapshot = await self._async_measure_memory_with_growth_tracking(
                            memory_baseline, previous_snapshot
                        )

                        # Update previous snapshot for next iteration
                        previous_snapshot = current_snapshot

                    else:
                        usage = await self._async_measure_memory(memory_baseline)
                    memory_usages.append(usage)
                    log_debug(f"Run {i + 1} - Memory usage: {usage:.6f} MiB (adjusted)")

                    status.stop()

        # 4. Collect results
        self.result = PerformanceResult(run_times=run_times, memory_usages=memory_usages)

        # 5. Save result to file if requested
        if self.file_path_to_save_results is not None and self.result is not None:
            store_result_in_file(
                file_path=self.file_path_to_save_results,
                name=self.name,
                eval_id=self.eval_id,
                result=self.result,
            )

        # 6. Print results if requested
        if self.print_results or print_results:
            self.result.print_results(console, measure_memory=self.measure_memory, measure_runtime=self.measure_runtime)
        if self.print_summary or print_summary:
            self.result.print_summary(console, measure_memory=self.measure_memory, measure_runtime=self.measure_runtime)

        # 7. Log results to the Agno platform if requested
        if self.db:
            eval_input = {
                "num_iterations": self.num_iterations,
                "warmup_runs": self.warmup_runs,
            }

            await async_log_eval(
                db=self.db,
                run_id=self.eval_id,  # type: ignore
                run_data=self._parse_eval_run_data(),
                eval_type=EvalType.PERFORMANCE,
                name=self.name if self.name is not None else None,
                evaluated_component_name=self.func.__name__,
                agent_id=self.agent_id,
                team_id=self.team_id,
                model_id=self.model_id,
                model_provider=self.model_provider,
                eval_input=eval_input,
            )

        if self.telemetry:
            from agno.api.evals import EvalRunCreate, async_create_eval_run_telemetry

            await async_create_eval_run_telemetry(
                eval_run=EvalRunCreate(
                    run_id=self.eval_id, eval_type=EvalType.PERFORMANCE, data=self._get_telemetry_data()
                ),
            )

        log_debug(f"*********** Evaluation End: {self.eval_id} ***********")
        return self.result

    def _get_telemetry_data(self) -> Dict[str, Any]:
        """Get the telemetry data for the evaluation"""
        return {
            "model_id": self.model_id,
            "model_provider": self.model_provider,
            "num_iterations": self.num_iterations,
            "warmup_runs": self.warmup_runs,
            "measure_memory": self.measure_memory,
            "measure_runtime": self.measure_runtime,
        }
