Skip to content

Commit 67f62f0

Browse files
dorellangcopybara-github
authored andcommitted
Measure Time to Create & Ready for Agent Runtime.
PiperOrigin-RevId: 941305634
1 parent 98c7176 commit 67f62f0

5 files changed

Lines changed: 131 additions & 3 deletions

File tree

perfkitbenchmarker/ai_agent_benchmark_helper.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ def Prepare(self) -> None:
122122
self.agent_service.agent_config = self._GetDefaultAgentConfig()
123123

124124
self.BeforeCreateAgent()
125+
self.spec.always_call_cleanup = True
125126
self.agent_service.Create()
126127
self.UploadValidatorScript()
127128
self.PostPrepare()

perfkitbenchmarker/data/agents/common_utils.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import abc
44
import collections.abc
55
import json
6-
from typing import Any
6+
from typing import Any, Self
77
import urllib.parse
88

99
from absl import logging
@@ -38,6 +38,19 @@ class PromptConfig[AgentConfigT](BasePromptConfig):
3838
run_uri: str
3939
agent_config: AgentConfigT
4040

41+
@classmethod
42+
def create_for_initial_prompt(cls, deployment_config: Any) -> Self:
43+
return cls(
44+
agent=deployment_config.agent,
45+
framework=deployment_config.framework,
46+
prompt=deployment_config.initial_prompt,
47+
output_dir="",
48+
session_id="timetoreadysession",
49+
user_id="timetoreadyuser",
50+
run_uri=deployment_config.run_uri,
51+
agent_config=deployment_config.agent_config,
52+
)
53+
4154

4255
class BaseEndpoint(abc.ABC):
4356
"""An abstract interface for executing agents across different environments.

perfkitbenchmarker/data/agents/deploy_agent_engine.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44
"""
55

66
import argparse
7+
import asyncio
78
import importlib
89
import os
10+
import time
911
from typing import Any
1012

13+
import common_utils
1114
import pydantic
1215
import vertexai
1316
import yaml
@@ -33,6 +36,7 @@ class DeploymentConfig[AgentConfigT](BaseDeploymentConfig):
3336
staging_bucket: str
3437
run_uri: str
3538
agent_config: AgentConfigT
39+
initial_prompt: str | None = None
3640

3741

3842
def _import_agent_module(agent: str, framework: str) -> Any:
@@ -41,6 +45,31 @@ def _import_agent_module(agent: str, framework: str) -> Any:
4145
return importlib.import_module(module_name)
4246

4347

48+
async def _measure_time_to_ready[AgentConfigT](
49+
remote_agent: Any,
50+
handler: Any,
51+
config: DeploymentConfig[AgentConfigT],
52+
) -> float | None:
53+
"""Measures the time to first chunk."""
54+
print("Sending initial prompt...")
55+
endpoint = handler.create_endpoint(remote_agent)
56+
prompt_config = common_utils.PromptConfig[
57+
AgentConfigT
58+
].create_for_initial_prompt(config)
59+
first_chunk_time = None
60+
try:
61+
async for _ in endpoint.stream_execute(prompt_config=prompt_config):
62+
if first_chunk_time is None:
63+
first_chunk_time = time.monotonic()
64+
return first_chunk_time
65+
except Exception as e: # pylint: disable=broad-exception-caught
66+
# Since the agent has already been created, it's better to let this script
67+
# finish normally, so the resource is marked as created and can be cleaned
68+
# up normally.
69+
print(f"Error measuring time to ready: {e}")
70+
return None
71+
72+
4473
def run_deployment[AgentConfigT](
4574
config: DeploymentConfig[AgentConfigT], module: Any
4675
) -> None:
@@ -75,13 +104,25 @@ def run_deployment[AgentConfigT](
75104
"display_name": display_name,
76105
}
77106

107+
create_start = time.monotonic()
78108
remote_agent = client.agent_engines.create(
79109
agent=agent_to_deploy,
80110
config=deploy_config,
81111
)
112+
create_time = time.monotonic() - create_start
113+
print(f"Time to Create: {create_time}")
114+
82115
print("Successfully deployed Agent Engine!")
83116
print(f"Resource name: {remote_agent.api_resource.name}")
84117

118+
if config.initial_prompt:
119+
first_chunk_time = asyncio.run(
120+
_measure_time_to_ready(remote_agent, handler, config)
121+
)
122+
if first_chunk_time is not None:
123+
ready_time = first_chunk_time - create_start
124+
print(f"Time to Ready: {ready_time}")
125+
85126

86127
def main() -> None:
87128
parser = argparse.ArgumentParser(

perfkitbenchmarker/providers/gcp/gcp_ai_agent_service.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from perfkitbenchmarker import context
1313
from perfkitbenchmarker import data
1414
from perfkitbenchmarker import errors
15+
from perfkitbenchmarker import sample
1516
from perfkitbenchmarker import vm_util
1617
from perfkitbenchmarker.providers.gcp import gce_virtual_machine
1718
from perfkitbenchmarker.providers.gcp import gcs
@@ -363,6 +364,8 @@ def __init__(self, client_vm, ai_agent_spec):
363364
self._remote_agent_name = None
364365
self._staging_bucket = self.base_dir
365366
self.spec = ai_agent_spec
367+
self._time_to_create: float | None = None
368+
self._time_to_ready: float | None = None
366369

367370
@override
368371
def _StageAgentCode(self):
@@ -419,6 +422,9 @@ def _GetDeploymentConfig(self) -> dict[str, Any]:
419422
'staging_bucket': self._staging_bucket,
420423
'agent_config': self.agent_config,
421424
})
425+
initial_prompt = self._GetInitialPromptText()
426+
if initial_prompt:
427+
config['initial_prompt'] = initial_prompt
422428
return config
423429

424430
def _Create(self):
@@ -446,12 +452,17 @@ def _Create(self):
446452
command = ' && '.join(command_parts)
447453
stdout, _ = self.client_vm.RemoteCommand(command)
448454

449-
# 5. Parse output to get remote agent name
455+
# 5. Parse output to get remote agent name and latencies
450456
for line in stdout.split('\n'):
451457
if line.startswith('Resource name: '):
452458
_, _, agent_name = line.partition('Resource name: ')
453459
self._remote_agent_name = agent_name.strip()
454-
break
460+
elif line.startswith('Time to Create: '):
461+
_, _, time_str = line.partition('Time to Create: ')
462+
self._time_to_create = float(time_str.strip())
463+
elif line.startswith('Time to Ready: '):
464+
_, _, time_str = line.partition('Time to Ready: ')
465+
self._time_to_ready = float(time_str.strip())
455466

456467
if not self._remote_agent_name:
457468
raise errors.Benchmarks.PrepareException(
@@ -464,6 +475,16 @@ def _Create(self):
464475
self._remote_agent_name,
465476
)
466477

478+
def _PostCreate(self):
479+
if (
480+
ai_agent_service.AI_AGENT_INITIAL_PROMPT_URL.value
481+
and not self._time_to_ready
482+
):
483+
raise errors.Benchmarks.PrepareException(
484+
'Initial prompt was explictly passed, but failed to get remote ready'
485+
' time from deploy script output.'
486+
)
487+
467488
def _Delete(self):
468489
"""Deletes the remote agent."""
469490
if not self._remote_agent_name:
@@ -600,3 +621,38 @@ def Execute(
600621
'Agent execution finished. Raw output:\n%s',
601622
stdout,
602623
)
624+
625+
@override
626+
def GetSamples(self):
627+
samples = super().GetSamples()
628+
create_time_sample = [s for s in samples if s.metric == 'Time to Create'][0]
629+
resource_type = create_time_sample.metadata.get('resource_type')
630+
resource_class = create_time_sample.metadata.get('resource_class')
631+
metadata = {
632+
'resource_type': resource_type,
633+
'resource_class': resource_class,
634+
}
635+
636+
# Remove existing samples for 'Time to Create' and 'Time to Ready' if any
637+
samples = [
638+
s
639+
for s in samples
640+
if s.metric not in ('Time to Create', 'Time to Ready')
641+
]
642+
643+
if self._time_to_create is not None:
644+
samples.append(
645+
sample.Sample(
646+
'Time to Create',
647+
self._time_to_create,
648+
'seconds',
649+
metadata=metadata,
650+
)
651+
)
652+
if self._time_to_ready is not None:
653+
samples.append(
654+
sample.Sample(
655+
'Time to Ready', self._time_to_ready, 'seconds', metadata=metadata
656+
)
657+
)
658+
return samples

perfkitbenchmarker/resources/ai_agent_service.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@
1414

1515
FLAGS = flags.FLAGS
1616

17+
AI_AGENT_INITIAL_PROMPT_URL = flags.DEFINE_string(
18+
'ai_agent_initial_prompt_url',
19+
None,
20+
'Object storage URL (e.g. gs://bucket/prompt.txt) to an initial prompt.',
21+
)
22+
1723

1824
def GetAiAgentServiceClass(cloud: str, deployment_type: str):
1925
"""Returns the correct AI agent service class based on cloud/type."""
@@ -66,6 +72,17 @@ def _GetDeploymentConfig(self) -> dict[str, Any]:
6672
"""Gets config dict for deployment/creation."""
6773
return {'run_uri': FLAGS.run_uri}
6874

75+
def _GetInitialPromptText(self) -> str | None:
76+
"""Fetches the initial prompt text from object storage."""
77+
url = AI_AGENT_INITIAL_PROMPT_URL.value
78+
if not url:
79+
return None
80+
81+
local_path = vm_util.PrependTempDir('initial_prompt.txt')
82+
self.storage_service.Copy(url, local_path)
83+
with open(local_path, 'r') as f:
84+
return f.read().strip()
85+
6986
def _GetRunConfig(
7087
self,
7188
output_dir: str,

0 commit comments

Comments
 (0)