Advanced
Performance Tuning

Performance Tuning

Optimize SDK performance for large-scale operations. Throughput and limits are ultimately bounded by the DealCloud API and intapp-rest-client (RestClient, retries, concurrency); see Advanced configuration.

Key Principles

  1. Minimize data transfer - Request only needed fields
  2. Use streaming for large data - Avoid loading all into memory
  3. Batch operations - Group related writes
  4. Cache schema - Avoid repeated schema calls
  5. Parallel where safe - Use concurrency within limits

Reading Data

Select Only Needed Fields

# ❌ Bad: Fetches all fields
data = dc.read_data("Company", output="pandas")
 
# ✅ Good: Fetches only needed fields
data = dc.read_data(
    "Company",
    output="pandas",
    fields=["CompanyName", "Revenue", "Status"]
)

Use Queries for Server-Side Filtering

# ❌ Bad: Fetch all, filter locally
data = dc.read_data("Company", output="pandas")
active = data[data["Status"] == "Active"]
 
# ✅ Good: Server-side filter
active = dc.read_data(
    "Company",
    output="pandas",
    query="{Status: 'Active'}"
)

Use streaming for large datasets

read_data_streaming() yields batches (list[dict]), not individual rows:

# Bad: load everything
data = dc.read_data("LargeObject", output="list")
 
# Good: process batch by batch
for batch in dc.read_data_streaming("LargeObject", fields=["EntryId", "Name"]):
    for row in batch:
        process(row)

Compare Memory Usage

import tracemalloc
 
# Standard read
tracemalloc.start()
data = dc.read_data("Company", output="list")
current, peak = tracemalloc.get_traced_memory()
print(f"Standard: {peak / 1024 / 1024:.1f} MB")
tracemalloc.stop()
 
# Streaming read (each iteration is a batch)
tracemalloc.start()
for batch in dc.read_data_streaming("Company"):
    pass
current, peak = tracemalloc.get_traced_memory()
print(f"Streaming: {peak / 1024 / 1024:.1f} MB")
tracemalloc.stop()

SDK profiling helpers

The SDK exposes optional pandas-oriented profiling utilities for analyzing files or DataFrames before ingest or migration (suggested field types, null counts, choice detection). They complement the tracemalloc patterns above by focusing on column semantics, not Python heap usage.

See Utilities & helpers for profile_dataframe, profile_files, and related symbols.

Writing Data

Batch Records Together

# ❌ Bad: One API call per record
for record in records:
    dc.insert_data("Company", [record])
 
# ✅ Good: One API call for all records
dc.insert_data("Company", records)

Optimal Batch Sizes

OperationRecommended BatchNotes
Insert500-1000Balance between memory and calls
Update500-1000Same as insert
Delete5000-10000Deletes are lightweight
def batch_insert(dc, object_id, records, batch_size=500):
    """Insert records in optimal batches."""
    results = []
    
    for i in range(0, len(records), batch_size):
        batch = records[i:i + batch_size]
        result = dc.insert_data(object_id, batch)
        results.extend(result)
        print(f"Inserted {len(results)}/{len(records)}")
    
    return results

Only Include Changed Fields in Updates

# ❌ Bad: Send all fields
dc.update_data("Company", [
    {"EntryId": 123, "Name": "...", "Industry": "...", "Revenue": 1000}  # Unchanged fields
])
 
# ✅ Good: Send only changed fields
dc.update_data("Company", [
    {"EntryId": 123, "Revenue": 1000}  # Only the change
])

Schema Caching

Cache Schema Locally

# ❌ Bad: Repeated schema calls
for obj in objects:
    schema = dc.get_schema()  # API call each iteration
    process(obj, schema)
 
# ✅ Good: Cache once
schema = dc.get_schema()
for obj in objects:
    process(obj, schema)

Field Lookup Cache

class FieldCache:
    def __init__(self, dc):
        self._cache = {}
        self._dc = dc
    
    def get_field(self, object_id, field_name):
        key = f"{object_id}.{field_name}"
        if key not in self._cache:
            fields = self._dc.get_fields(object_id)
            for f in fields:
                self._cache[f"{object_id}.{f.apiName}"] = f
        return self._cache.get(key)
 
# Usage
cache = FieldCache(dc)
field = cache.get_field("Company", "Revenue")  # First call: API
field = cache.get_field("Company", "Status")   # Cache hit!

Reference Caching

from dealcloud_sdk import ReferenceCache
 
# Create shared cache
cache = ReferenceCache(max_entries=10000)
 
# Use across multiple reads
for obj in ["Contact", "Deal", "Interaction"]:
    data = dc.read_data(
        obj,
        output="pandas",
        reference_format=ReferenceFormat.NAME,
        reference_cache=cache  # Shared cache
    )

Concurrency

Configure Concurrency Limits

from dealcloud_sdk import DealCloudConfig, ConcurrencyLimits
 
config = DealCloudConfig(
    siteUrl="...",
    clientId=12345,
    clientSecret="...",
    concurrencyLimits=ConcurrencyLimits(read=4, create=2, delete=2),
)
 
dc = DealCloud.from_config_object(config)
⚠️

The Data API is commonly limited to roughly five requests per second per site. Raising ConcurrencyLimits too high often triggers 429; tune with retry settings and realistic batch sizes.

Parallel pagination within a single read

The read concurrency limit also controls how many pages a single read_data() / read_data_streaming() call fetches in parallel (pages are still returned in order). With the default of 4, a large multi-page object is read up to ~4x faster than strictly sequential paging while staying under the ~5 req/s cap. Override per call with max_concurrency= on the streaming methods, or set it to 1 to force sequential fetching.

# Reads pages in parallel up to read_concurrency (default 4)
data = dc.read_data("LargeObject", output="polars")
 
# Cap parallelism for this streaming read
for batch in dc.read_data_streaming("LargeObject", max_concurrency=2):
    process(batch)

Parallel object processing

aread_data_streaming() also yields batches. Parallelize across objects with care (API rate limits):

import asyncio
 
async def export_one_object(dc, obj_name):
    rows = []
    async for batch in dc.aread_data_streaming(obj_name):
        rows.extend(batch)
    return obj_name, rows
 
async def parallel_export(dc, objects):
    return dict(await asyncio.gather(*[export_one_object(dc, o) for o in objects]))
 
# asyncio.run(parallel_export(dc, ["Company", "Contact", "Deal"]))

Configuration Tuning

Page Size

config = DealCloudConfig(
    # ...credentials
    querySettings={
        "pageSize": 2000,  # Larger pages, fewer requests
    }
)
Page SizeMemoryAPI CallsBest For
100LowManyMemory-constrained
1000MediumBalancedGeneral use (default)
5000HighFewLarge exports, high bandwidth

Timeout

config = DealCloudConfig(
    # ...credentials
    connectorTimeoutSeconds=180  # 3 minutes for large operations
)

Monitoring Performance

import time
 
class PerformanceMonitor:
    def __init__(self):
        self.metrics = {}
    
    def time_operation(self, name):
        """Context manager to time operations."""
        class Timer:
            def __init__(self, monitor, name):
                self.monitor = monitor
                self.name = name
            
            def __enter__(self):
                self.start = time.time()
                return self
            
            def __exit__(self, *args):
                elapsed = time.time() - self.start
                self.monitor.metrics[self.name] = elapsed
                print(f"{self.name}: {elapsed:.2f}s")
        
        return Timer(self, name)
    
    def report(self):
        total = sum(self.metrics.values())
        print(f"\nTotal time: {total:.2f}s")
        for name, elapsed in sorted(self.metrics.items(), key=lambda x: -x[1]):
            pct = elapsed / total * 100
            print(f"  {name}: {elapsed:.2f}s ({pct:.1f}%)")
 
# Usage
monitor = PerformanceMonitor()
 
with monitor.time_operation("read_companies"):
    companies = dc.read_data("Company", output="list")
 
with monitor.time_operation("read_contacts"):
    contacts = dc.read_data("Contact", output="list")
 
monitor.report()

Checklist

Performance Optimization Checklist

  • Use fields= to limit returned columns
  • Use query= for server-side filtering
  • Use streaming for > 10k records
  • Cache schema and references
  • Batch writes appropriately
  • Configure timeout for large operations
  • Monitor and profile slow operations

Related