chore: import upstream snapshot with attribution
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,494 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Tests for CachePerformanceAnalyzer."""
|
||||
|
||||
import time
|
||||
from unittest.mock import AsyncMock
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from google.adk.events.event import Event
|
||||
from google.adk.models.cache_metadata import CacheMetadata
|
||||
from google.adk.sessions.base_session_service import BaseSessionService
|
||||
from google.adk.sessions.session import Session
|
||||
from google.adk.utils.cache_performance_analyzer import CachePerformanceAnalyzer
|
||||
from google.genai import types
|
||||
import pytest
|
||||
|
||||
|
||||
class TestCachePerformanceAnalyzer:
|
||||
"""Test suite for CachePerformanceAnalyzer."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.mock_session_service = MagicMock(spec=BaseSessionService)
|
||||
self.analyzer = CachePerformanceAnalyzer(self.mock_session_service)
|
||||
|
||||
def create_cache_metadata(
|
||||
self, invocations_used=1, cache_name="test-cache", contents_count=5
|
||||
):
|
||||
"""Helper to create test CacheMetadata."""
|
||||
return CacheMetadata(
|
||||
cache_name=(
|
||||
f"projects/test/locations/us-central1/cachedContents/{cache_name}"
|
||||
),
|
||||
expire_time=time.time() + 1800,
|
||||
fingerprint="test_fingerprint",
|
||||
invocations_used=invocations_used,
|
||||
contents_count=contents_count,
|
||||
created_at=time.time() - 600,
|
||||
)
|
||||
|
||||
def create_mock_usage_metadata(
|
||||
self, prompt_tokens=1000, cached_tokens=500, candidates_tokens=100
|
||||
):
|
||||
"""Helper to create mock usage metadata."""
|
||||
return types.GenerateContentResponseUsageMetadata(
|
||||
prompt_token_count=prompt_tokens,
|
||||
cached_content_token_count=cached_tokens,
|
||||
candidates_token_count=candidates_tokens,
|
||||
total_token_count=prompt_tokens + candidates_tokens,
|
||||
)
|
||||
|
||||
def create_mock_event(
|
||||
self, author="test_agent", cache_metadata=None, usage_metadata=None
|
||||
):
|
||||
"""Helper to create mock event."""
|
||||
event = Event(author=author, cache_metadata=cache_metadata)
|
||||
if usage_metadata:
|
||||
event.usage_metadata = usage_metadata
|
||||
return event
|
||||
|
||||
def test_init(self):
|
||||
"""Test analyzer initialization."""
|
||||
assert self.analyzer.session_service == self.mock_session_service
|
||||
|
||||
async def test_get_agent_cache_history_empty_session(self):
|
||||
"""Test getting cache history from empty session."""
|
||||
mock_session = Session(
|
||||
id="test_session",
|
||||
app_name="test_app",
|
||||
user_id="test_user",
|
||||
events=[],
|
||||
)
|
||||
self.mock_session_service.get_session = AsyncMock(return_value=mock_session)
|
||||
|
||||
result = await self.analyzer._get_agent_cache_history(
|
||||
"test_session", "test_user", "test_app", "test_agent"
|
||||
)
|
||||
|
||||
assert result == []
|
||||
|
||||
async def test_get_agent_cache_history_no_cache_events(self):
|
||||
"""Test getting cache history when no events have cache metadata."""
|
||||
events = [
|
||||
self.create_mock_event(author="test_agent"),
|
||||
self.create_mock_event(author="other_agent"),
|
||||
self.create_mock_event(author="test_agent"),
|
||||
]
|
||||
|
||||
mock_session = Session(
|
||||
id="test_session",
|
||||
app_name="test_app",
|
||||
user_id="test_user",
|
||||
events=events,
|
||||
)
|
||||
self.mock_session_service.get_session = AsyncMock(return_value=mock_session)
|
||||
|
||||
result = await self.analyzer._get_agent_cache_history(
|
||||
"test_session", "test_user", "test_app", "test_agent"
|
||||
)
|
||||
|
||||
assert result == []
|
||||
|
||||
async def test_get_agent_cache_history_specific_agent(self):
|
||||
"""Test getting cache history for specific agent."""
|
||||
cache1 = self.create_cache_metadata(invocations_used=1, cache_name="cache1")
|
||||
cache2 = self.create_cache_metadata(invocations_used=3, cache_name="cache2")
|
||||
cache3 = self.create_cache_metadata(invocations_used=5, cache_name="cache3")
|
||||
|
||||
events = [
|
||||
self.create_mock_event(author="test_agent", cache_metadata=cache1),
|
||||
self.create_mock_event(author="other_agent", cache_metadata=cache2),
|
||||
self.create_mock_event(author="test_agent", cache_metadata=cache3),
|
||||
self.create_mock_event(author="test_agent"), # No cache metadata
|
||||
]
|
||||
|
||||
mock_session = Session(
|
||||
id="test_session",
|
||||
app_name="test_app",
|
||||
user_id="test_user",
|
||||
events=events,
|
||||
)
|
||||
self.mock_session_service.get_session = AsyncMock(return_value=mock_session)
|
||||
|
||||
result = await self.analyzer._get_agent_cache_history(
|
||||
"test_session", "test_user", "test_app", "test_agent"
|
||||
)
|
||||
|
||||
# Should only return cache metadata for test_agent
|
||||
assert len(result) == 2
|
||||
assert result[0] == cache1
|
||||
assert result[1] == cache3
|
||||
|
||||
async def test_get_agent_cache_history_all_agents(self):
|
||||
"""Test getting cache history for all agents."""
|
||||
cache1 = self.create_cache_metadata(invocations_used=1, cache_name="cache1")
|
||||
cache2 = self.create_cache_metadata(invocations_used=3, cache_name="cache2")
|
||||
|
||||
events = [
|
||||
self.create_mock_event(author="agent1", cache_metadata=cache1),
|
||||
self.create_mock_event(author="agent2", cache_metadata=cache2),
|
||||
self.create_mock_event(author="agent1"), # No cache metadata
|
||||
]
|
||||
|
||||
mock_session = Session(
|
||||
id="test_session",
|
||||
app_name="test_app",
|
||||
user_id="test_user",
|
||||
events=events,
|
||||
)
|
||||
self.mock_session_service.get_session = AsyncMock(return_value=mock_session)
|
||||
|
||||
# Pass None for agent_name to get all agents
|
||||
result = await self.analyzer._get_agent_cache_history(
|
||||
"test_session", "test_user", "test_app", None
|
||||
)
|
||||
|
||||
# Should return cache metadata for all agents
|
||||
assert len(result) == 2
|
||||
assert result[0] == cache1
|
||||
assert result[1] == cache2
|
||||
|
||||
async def test_analyze_agent_cache_performance_no_cache_data(self):
|
||||
"""Test analysis with no cache data."""
|
||||
mock_session = Session(
|
||||
id="test_session",
|
||||
app_name="test_app",
|
||||
user_id="test_user",
|
||||
events=[],
|
||||
)
|
||||
self.mock_session_service.get_session = AsyncMock(return_value=mock_session)
|
||||
|
||||
result = await self.analyzer.analyze_agent_cache_performance(
|
||||
"test_session", "test_user", "test_app", "test_agent"
|
||||
)
|
||||
|
||||
assert result["status"] == "no_cache_data"
|
||||
|
||||
async def test_analyze_agent_cache_performance_with_cache_data(self):
|
||||
"""Test comprehensive analysis with cache data and token metrics."""
|
||||
cache1 = self.create_cache_metadata(invocations_used=2, cache_name="cache1")
|
||||
cache2 = self.create_cache_metadata(invocations_used=5, cache_name="cache2")
|
||||
cache3 = self.create_cache_metadata(invocations_used=8, cache_name="cache3")
|
||||
|
||||
usage1 = self.create_mock_usage_metadata(
|
||||
prompt_tokens=1000, cached_tokens=800
|
||||
)
|
||||
usage2 = self.create_mock_usage_metadata(
|
||||
prompt_tokens=1500, cached_tokens=1200
|
||||
)
|
||||
usage3 = self.create_mock_usage_metadata(prompt_tokens=800, cached_tokens=0)
|
||||
|
||||
events = [
|
||||
self.create_mock_event(
|
||||
author="test_agent", cache_metadata=cache1, usage_metadata=usage1
|
||||
),
|
||||
self.create_mock_event(author="other_agent", cache_metadata=cache2),
|
||||
self.create_mock_event(
|
||||
author="test_agent", cache_metadata=cache2, usage_metadata=usage2
|
||||
),
|
||||
self.create_mock_event(
|
||||
author="test_agent", cache_metadata=cache3, usage_metadata=usage3
|
||||
),
|
||||
]
|
||||
|
||||
mock_session = Session(
|
||||
id="test_session",
|
||||
app_name="test_app",
|
||||
user_id="test_user",
|
||||
events=events,
|
||||
)
|
||||
self.mock_session_service.get_session = AsyncMock(return_value=mock_session)
|
||||
|
||||
result = await self.analyzer.analyze_agent_cache_performance(
|
||||
"test_session", "test_user", "test_app", "test_agent"
|
||||
)
|
||||
|
||||
# Basic cache metrics
|
||||
assert result["status"] == "active"
|
||||
assert result["requests_with_cache"] == 3
|
||||
assert result["cache_refreshes"] == 3 # 3 unique cache names
|
||||
assert result["total_invocations"] == 15 # 2 + 5 + 8
|
||||
|
||||
expected_avg_invocations = (2 + 5 + 8) / 3 # 5.0
|
||||
assert result["avg_invocations_used"] == expected_avg_invocations
|
||||
|
||||
# Token metrics
|
||||
assert result["total_prompt_tokens"] == 3300 # 1000 + 1500 + 800
|
||||
assert result["total_cached_tokens"] == 2000 # 800 + 1200 + 0
|
||||
assert result["total_requests"] == 3
|
||||
assert (
|
||||
result["requests_with_cache_hits"] == 2
|
||||
) # Only first two have cached tokens
|
||||
|
||||
# Calculated metrics
|
||||
expected_hit_ratio = (2000 / 3300) * 100 # ~60.6%
|
||||
expected_utilization = (2 / 3) * 100 # ~66.7%
|
||||
expected_avg_cached = 2000 / 3 # ~666.7
|
||||
|
||||
assert abs(result["cache_hit_ratio_percent"] - expected_hit_ratio) < 0.01
|
||||
assert (
|
||||
abs(result["cache_utilization_ratio_percent"] - expected_utilization)
|
||||
< 0.01
|
||||
)
|
||||
assert (
|
||||
abs(result["avg_cached_tokens_per_request"] - expected_avg_cached)
|
||||
< 0.01
|
||||
)
|
||||
|
||||
async def test_analyze_agent_cache_performance_single_cache(self):
|
||||
"""Test analysis with single cache instance."""
|
||||
cache = self.create_cache_metadata(
|
||||
invocations_used=10, cache_name="single_cache"
|
||||
)
|
||||
usage = self.create_mock_usage_metadata(
|
||||
prompt_tokens=2000, cached_tokens=1500
|
||||
)
|
||||
|
||||
events = [
|
||||
self.create_mock_event(
|
||||
author="test_agent", cache_metadata=cache, usage_metadata=usage
|
||||
),
|
||||
]
|
||||
|
||||
mock_session = Session(
|
||||
id="test_session",
|
||||
app_name="test_app",
|
||||
user_id="test_user",
|
||||
events=events,
|
||||
)
|
||||
self.mock_session_service.get_session = AsyncMock(return_value=mock_session)
|
||||
|
||||
result = await self.analyzer.analyze_agent_cache_performance(
|
||||
"test_session", "test_user", "test_app", "test_agent"
|
||||
)
|
||||
|
||||
assert result["status"] == "active"
|
||||
assert result["requests_with_cache"] == 1
|
||||
assert result["avg_invocations_used"] == 10.0
|
||||
assert result["cache_refreshes"] == 1
|
||||
assert result["total_invocations"] == 10
|
||||
assert result["latest_cache"] == cache.cache_name
|
||||
|
||||
# Token metrics for single request
|
||||
assert result["total_prompt_tokens"] == 2000
|
||||
assert result["total_cached_tokens"] == 1500
|
||||
assert result["cache_hit_ratio_percent"] == 75.0 # 1500/2000 * 100
|
||||
assert result["cache_utilization_ratio_percent"] == 100.0 # 1/1 * 100
|
||||
assert result["avg_cached_tokens_per_request"] == 1500.0
|
||||
|
||||
async def test_analyze_agent_cache_performance_no_token_data(self):
|
||||
"""Test analysis when events have no usage_metadata."""
|
||||
cache = self.create_cache_metadata(invocations_used=5)
|
||||
|
||||
events = [
|
||||
self.create_mock_event(author="test_agent", cache_metadata=cache),
|
||||
]
|
||||
|
||||
mock_session = Session(
|
||||
id="test_session",
|
||||
app_name="test_app",
|
||||
user_id="test_user",
|
||||
events=events,
|
||||
)
|
||||
self.mock_session_service.get_session = AsyncMock(return_value=mock_session)
|
||||
|
||||
result = await self.analyzer.analyze_agent_cache_performance(
|
||||
"test_session", "test_user", "test_app", "test_agent"
|
||||
)
|
||||
|
||||
# Should still work but with zero token metrics
|
||||
assert result["status"] == "active"
|
||||
assert result["requests_with_cache"] == 1
|
||||
assert result["total_prompt_tokens"] == 0
|
||||
assert result["total_cached_tokens"] == 0
|
||||
assert result["cache_hit_ratio_percent"] == 0.0
|
||||
assert result["cache_utilization_ratio_percent"] == 0.0
|
||||
assert result["avg_cached_tokens_per_request"] == 0.0
|
||||
|
||||
async def test_analyze_agent_cache_performance_zero_invocations(self):
|
||||
"""Test analysis with zero invocations."""
|
||||
cache = self.create_cache_metadata(
|
||||
invocations_used=0, cache_name="zero_cache"
|
||||
)
|
||||
usage = self.create_mock_usage_metadata(
|
||||
prompt_tokens=1000, cached_tokens=500
|
||||
)
|
||||
|
||||
events = [
|
||||
self.create_mock_event(
|
||||
author="test_agent", cache_metadata=cache, usage_metadata=usage
|
||||
),
|
||||
]
|
||||
|
||||
mock_session = Session(
|
||||
id="test_session",
|
||||
app_name="test_app",
|
||||
user_id="test_user",
|
||||
events=events,
|
||||
)
|
||||
self.mock_session_service.get_session = AsyncMock(return_value=mock_session)
|
||||
|
||||
result = await self.analyzer.analyze_agent_cache_performance(
|
||||
"test_session", "test_user", "test_app", "test_agent"
|
||||
)
|
||||
|
||||
assert result["status"] == "active"
|
||||
assert result["avg_invocations_used"] == 0.0
|
||||
assert result["total_invocations"] == 0
|
||||
|
||||
# Token metrics should still work
|
||||
assert result["total_prompt_tokens"] == 1000
|
||||
assert result["total_cached_tokens"] == 500
|
||||
|
||||
async def test_session_service_integration(self):
|
||||
"""Test integration with session service."""
|
||||
cache_metadata = self.create_cache_metadata(invocations_used=7)
|
||||
|
||||
events = [
|
||||
self.create_mock_event(
|
||||
author="integration_agent", cache_metadata=cache_metadata
|
||||
),
|
||||
]
|
||||
|
||||
mock_session = Session(
|
||||
id="integration_session",
|
||||
app_name="integration_app",
|
||||
user_id="integration_user",
|
||||
events=events,
|
||||
)
|
||||
|
||||
# Configure the mock to return the session
|
||||
self.mock_session_service.get_session = AsyncMock(return_value=mock_session)
|
||||
|
||||
result = await self.analyzer.analyze_agent_cache_performance(
|
||||
"integration_session",
|
||||
"integration_user",
|
||||
"integration_app",
|
||||
"integration_agent",
|
||||
)
|
||||
|
||||
# Verify the session service was called with correct parameters (twice internally)
|
||||
assert self.mock_session_service.get_session.call_count == 2
|
||||
self.mock_session_service.get_session.assert_called_with(
|
||||
session_id="integration_session",
|
||||
app_name="integration_app",
|
||||
user_id="integration_user",
|
||||
)
|
||||
|
||||
assert result["status"] == "active"
|
||||
assert result["requests_with_cache"] == 1
|
||||
|
||||
async def test_analyze_agent_cache_performance_with_fingerprint_only(self):
|
||||
"""Fingerprint-only entries (cache_name=None, invocations_used=None) don't crash."""
|
||||
fp_only = CacheMetadata(fingerprint="fp", contents_count=3)
|
||||
active = self.create_cache_metadata(invocations_used=4, cache_name="active")
|
||||
fp_usage = self.create_mock_usage_metadata(
|
||||
prompt_tokens=1000, cached_tokens=0
|
||||
)
|
||||
active_usage = self.create_mock_usage_metadata(
|
||||
prompt_tokens=1000, cached_tokens=800
|
||||
)
|
||||
|
||||
events = [
|
||||
self.create_mock_event(
|
||||
author="test_agent",
|
||||
cache_metadata=fp_only,
|
||||
usage_metadata=fp_usage,
|
||||
),
|
||||
self.create_mock_event(
|
||||
author="test_agent",
|
||||
cache_metadata=active,
|
||||
usage_metadata=active_usage,
|
||||
),
|
||||
]
|
||||
mock_session = Session(
|
||||
id="test_session",
|
||||
app_name="test_app",
|
||||
user_id="test_user",
|
||||
events=events,
|
||||
)
|
||||
self.mock_session_service.get_session = AsyncMock(return_value=mock_session)
|
||||
|
||||
result = await self.analyzer.analyze_agent_cache_performance(
|
||||
"test_session", "test_user", "test_app", "test_agent"
|
||||
)
|
||||
|
||||
assert result["status"] == "active"
|
||||
assert result["total_requests"] == 2
|
||||
assert result["total_prompt_tokens"] == 2000
|
||||
assert result["total_cached_tokens"] == 800
|
||||
assert result["total_invocations"] == 4
|
||||
assert result["avg_invocations_used"] == 4.0
|
||||
assert result["cache_refreshes"] == 1
|
||||
assert result["requests_with_cache"] == 2
|
||||
|
||||
async def test_mixed_agents_filtering(self):
|
||||
"""Test that analysis correctly filters by agent name."""
|
||||
target_cache = self.create_cache_metadata(
|
||||
invocations_used=3, cache_name="target"
|
||||
)
|
||||
other_cache = self.create_cache_metadata(
|
||||
invocations_used=5, cache_name="other"
|
||||
)
|
||||
|
||||
target_usage = self.create_mock_usage_metadata(
|
||||
prompt_tokens=1000, cached_tokens=800
|
||||
)
|
||||
other_usage = self.create_mock_usage_metadata(
|
||||
prompt_tokens=2000, cached_tokens=1600
|
||||
)
|
||||
|
||||
events = [
|
||||
self.create_mock_event(
|
||||
author="target_agent",
|
||||
cache_metadata=target_cache,
|
||||
usage_metadata=target_usage,
|
||||
),
|
||||
self.create_mock_event(
|
||||
author="other_agent",
|
||||
cache_metadata=other_cache,
|
||||
usage_metadata=other_usage,
|
||||
),
|
||||
self.create_mock_event(author="target_agent"), # No cache data
|
||||
]
|
||||
|
||||
mock_session = Session(
|
||||
id="test_session",
|
||||
app_name="test_app",
|
||||
user_id="test_user",
|
||||
events=events,
|
||||
)
|
||||
self.mock_session_service.get_session = AsyncMock(return_value=mock_session)
|
||||
|
||||
result = await self.analyzer.analyze_agent_cache_performance(
|
||||
"test_session", "test_user", "test_app", "target_agent"
|
||||
)
|
||||
|
||||
# Should only include target_agent's data
|
||||
assert result["requests_with_cache"] == 1
|
||||
assert result["total_invocations"] == 3
|
||||
assert result["total_prompt_tokens"] == 1000 # Only target_agent's tokens
|
||||
assert result["total_cached_tokens"] == 800 # Only target_agent's tokens
|
||||
Reference in New Issue
Block a user