Skip to content

Commit

Permalink
Support approximate hypertable size
Browse files Browse the repository at this point in the history
If a lot of chunks are involved then the current pl/pgsql function
to compute the size of each chunk via a nested loop is pretty slow.
Additionally, the current functionality makes a system call to get the
file size on disk for each chunk everytime this function is called.
That again slows things down. We now have an approximate function which
is implemented in C to avoid the issues in the pl/pgsql function.
Additionally, this function also uses per backend caching using the
smgr layer to compute the approximate size cheaply. The PG cache
invalidation clears off the cached size for a chunk when DML happens
into it. That size cache is thus able to get the latest size in a
matter of minutes. Also, due to the backend caching, any long running
session will only fetch latest data for new or modified chunks and can
use the cached data (which is calculated afresh the first time around)
effectively for older chunks.
  • Loading branch information
nikkhils committed Feb 1, 2024
1 parent 1502bad commit fa2d99a
Show file tree
Hide file tree
Showing 12 changed files with 589 additions and 0 deletions.
1 change: 1 addition & 0 deletions .unreleased/pr_6463
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implements: #6463 Support approximate hypertable size
27 changes: 27 additions & 0 deletions sql/size_utils.sql
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ CREATE OR REPLACE FUNCTION _timescaledb_functions.relation_size(relation REGCLAS
RETURNS TABLE (total_size BIGINT, heap_size BIGINT, index_size BIGINT, toast_size BIGINT)
AS '@MODULE_PATHNAME@', 'ts_relation_size' LANGUAGE C VOLATILE;

CREATE OR REPLACE FUNCTION _timescaledb_functions.relation_approximate_size(relation REGCLASS)
RETURNS TABLE (total_size BIGINT, heap_size BIGINT, index_size BIGINT, toast_size BIGINT)
AS '@MODULE_PATHNAME@', 'ts_relation_approximate_size' LANGUAGE C STRICT VOLATILE;

CREATE OR REPLACE VIEW _timescaledb_internal.hypertable_chunk_local_size AS
SELECT
h.schema_name AS hypertable_schema,
Expand Down Expand Up @@ -169,6 +173,29 @@ $BODY$
FROM @[email protected]_detailed_size(hypertable);
$BODY$ SET search_path TO pg_catalog, pg_temp;

-- Get approximate relation size of hypertable
--
-- hypertable - hypertable to get approximate size of
--
-- Returns:
-- table_bytes - Approximate disk space used by hypertable
-- index_bytes - Approximate disk space used by indexes
-- toast_bytes - Approximate disk space of toast tables
-- total_bytes - Total approximate disk space used by the specified table, including all indexes and TOAST data
CREATE OR REPLACE FUNCTION @[email protected]_approximate_detailed_size(relation REGCLASS)
RETURNS TABLE (table_bytes BIGINT, index_bytes BIGINT, toast_bytes BIGINT, total_bytes BIGINT)
AS '@MODULE_PATHNAME@', 'ts_hypertable_approximate_size' LANGUAGE C VOLATILE;

--- returns approximate total-bytes for a hypertable (includes table + index)
CREATE OR REPLACE FUNCTION @[email protected]_approximate_size(
hypertable REGCLASS)
RETURNS BIGINT
LANGUAGE SQL VOLATILE STRICT AS
$BODY$
SELECT sum(total_bytes)::bigint
FROM @[email protected]_approximate_detailed_size(hypertable);
$BODY$ SET search_path TO pg_catalog, pg_temp;

CREATE OR REPLACE FUNCTION _timescaledb_functions.chunks_local_size(
schema_name_in name,
table_name_in name)
Expand Down
18 changes: 18 additions & 0 deletions sql/updates/latest-dev.sql
Original file line number Diff line number Diff line change
Expand Up @@ -427,3 +427,21 @@ ALTER EXTENSION timescaledb ADD TABLE _timescaledb_internal.job_errors;
ALTER EXTENSION timescaledb DROP TABLE _timescaledb_catalog.hypertable;
ALTER EXTENSION timescaledb ADD TABLE _timescaledb_catalog.hypertable;
SELECT pg_catalog.pg_extension_config_dump('_timescaledb_catalog.hypertable', 'WHERE id >= 1');

CREATE FUNCTION _timescaledb_functions.relation_approximate_size(relation REGCLASS)
RETURNS TABLE (total_size BIGINT, heap_size BIGINT, index_size BIGINT, toast_size BIGINT)
AS '@MODULE_PATHNAME@', 'ts_relation_approximate_size' LANGUAGE C STRICT VOLATILE;

CREATE FUNCTION @[email protected]_approximate_detailed_size(relation REGCLASS)
RETURNS TABLE (table_bytes BIGINT, index_bytes BIGINT, toast_bytes BIGINT, total_bytes BIGINT)
AS '@MODULE_PATHNAME@', 'ts_hypertable_approximate_size' LANGUAGE C VOLATILE;

--- returns approximate total-bytes for a hypertable (includes table + index)
CREATE FUNCTION @[email protected]_approximate_size(
hypertable REGCLASS)
RETURNS BIGINT
LANGUAGE SQL VOLATILE STRICT AS
$BODY$
SELECT sum(total_bytes)::bigint
FROM @[email protected]_approximate_detailed_size(hypertable);
$BODY$ SET search_path TO pg_catalog, pg_temp;
3 changes: 3 additions & 0 deletions sql/updates/reverse-dev.sql
Original file line number Diff line number Diff line change
Expand Up @@ -788,3 +788,6 @@ ALTER EXTENSION timescaledb DROP TABLE _timescaledb_catalog.hypertable;
ALTER EXTENSION timescaledb ADD TABLE _timescaledb_catalog.hypertable;
-- include this now in the config
SELECT pg_catalog.pg_extension_config_dump('_timescaledb_catalog.hypertable', '');
DROP FUNCTION IF EXISTS _timescaledb_functions.relation_approximate_size(relation REGCLASS);
DROP FUNCTION IF EXISTS @[email protected]_approximate_detailed_size(relation REGCLASS);
DROP FUNCTION IF EXISTS @[email protected]_approximate_size(hypertable REGCLASS);
268 changes: 268 additions & 0 deletions src/utils.c
Original file line number Diff line number Diff line change
Expand Up @@ -1025,6 +1025,274 @@ ts_relation_size_impl(Oid relid)
return relsize;
}

/*
* Try to get cached size for a provided relation across all forks. The
* size is returned in terms of number of blocks.
*
* The function calls the underlying smgrnblocks if there is no cached
* data. That call populates the cache for subsequent invocations. This
* cached data gets removed asynchronously by PG relcache invalidations
* and then the refresh/cache cycle repeats till the next invalidation.
*/
static int64
ts_try_relation_cached_size(Relation rel, bool verbose)
{
BlockNumber result = 0, nblocks = 0;
ForkNumber forkNum;
bool cached = true;

/* Get heap size, including FSM and VM */
for (forkNum = 0; forkNum <= MAX_FORKNUM; forkNum++)
{
#if PG14_LT
/* PG13 does not have smgr_cached_nblocks */
result = InvalidBlockNumber;
#else
result = RelationGetSmgr(rel)->smgr_cached_nblocks[forkNum];
#endif

if (result != InvalidBlockNumber)
{
nblocks += result;
}
else
{
if (smgrexists(RelationGetSmgr(rel), forkNum))
{
cached = false;
nblocks += smgrnblocks(RelationGetSmgr(rel), forkNum);
}
}
}

if (verbose)
ereport(DEBUG2,
(errmsg("%s for %s",
cached ? "Cached size used" : "Fetching actual size",
RelationGetRelationName(rel))));

/* convert the size into bytes and return */
return nblocks * BLCKSZ;
}

static RelationSize
ts_relation_approximate_size_impl(Oid relid)
{
RelationSize relsize = { 0 };
Relation rel;

DEBUG_WAITPOINT("relation_approximate_size_before_lock");
/* Open relation earlier to keep a lock during all function calls */
rel = try_relation_open(relid, AccessShareLock);

if (!rel)
return relsize;

Check warning on line 1089 in src/utils.c

View check run for this annotation

Codecov / codecov/patch

src/utils.c#L1089

Added line #L1089 was not covered by tests

/* Get the main heap size */
relsize.heap_size = ts_try_relation_cached_size(rel, false);

/* Get the size of the relation's indexes */
if (rel->rd_rel->relhasindex)
{
List *index_oids = RelationGetIndexList(rel);
ListCell *cell;

foreach (cell, index_oids)
{
Oid idxOid = lfirst_oid(cell);
Relation idxRel;

idxRel = relation_open(idxOid, AccessShareLock);
relsize.index_size += ts_try_relation_cached_size(idxRel, false);
relation_close(idxRel, AccessShareLock);
}
}

/* If there's an associated TOAST table, calculate the total size (including its indexes) */
if (OidIsValid(rel->rd_rel->reltoastrelid))
{
Relation toastRel;
List *index_oids;
ListCell *cell;

toastRel = relation_open(rel->rd_rel->reltoastrelid, AccessShareLock);
relsize.toast_size = ts_try_relation_cached_size(toastRel, false);

/* Get the indexes size of the TOAST relation */
index_oids = RelationGetIndexList(toastRel);
foreach (cell, index_oids)
{
Oid idxOid = lfirst_oid(cell);
Relation idxRel;

idxRel = relation_open(idxOid, AccessShareLock);
relsize.toast_size += ts_try_relation_cached_size(idxRel, false);
relation_close(idxRel, AccessShareLock);
}

relation_close(toastRel, AccessShareLock);
}

relation_close(rel, AccessShareLock);

/* Add up the total size based on the heap size, indexes and toast */
relsize.total_size = relsize.heap_size + relsize.index_size + relsize.toast_size;

return relsize;
}

TS_FUNCTION_INFO_V1(ts_relation_approximate_size);
Datum
ts_relation_approximate_size(PG_FUNCTION_ARGS)
{
Oid relid = PG_GETARG_OID(0);
RelationSize relsize = { 0 };
TupleDesc tupdesc;
HeapTuple tuple;
Datum values[4] = { 0 };
bool nulls[4] = { false };

/* Build a tuple descriptor for our result type */
if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("function returning record called in context "
"that cannot accept type record")));

/* check if object exists, return NULL otherwise */
if (get_rel_name(relid) == NULL)
PG_RETURN_NULL();

relsize = ts_relation_approximate_size_impl(relid);

tupdesc = BlessTupleDesc(tupdesc);

values[0] = Int64GetDatum(relsize.total_size);
values[1] = Int64GetDatum(relsize.heap_size);
values[2] = Int64GetDatum(relsize.index_size);
values[3] = Int64GetDatum(relsize.toast_size);

tuple = heap_form_tuple(tupdesc, values, nulls);

return HeapTupleGetDatum(tuple);
}

static void
init_scan_by_hypertable_id(ScanIterator *iterator, int32 hypertable_id)
{
iterator->ctx.index = catalog_get_index(ts_catalog_get(), CHUNK, CHUNK_HYPERTABLE_ID_INDEX);
ts_scan_iterator_scan_key_init(iterator,
Anum_chunk_hypertable_id_idx_hypertable_id,
BTEqualStrategyNumber,
F_INT4EQ,
Int32GetDatum(hypertable_id));
}

#define ADD_RELATIONSIZE(total, rel) \
do \
{ \
(total).heap_size += (rel).heap_size; \
(total).toast_size += (rel).toast_size; \
(total).index_size += (rel).index_size; \
(total).total_size += (rel).total_size; \
} while (0)

TS_FUNCTION_INFO_V1(ts_hypertable_approximate_size);
Datum
ts_hypertable_approximate_size(PG_FUNCTION_ARGS)
{
Oid relid = PG_ARGISNULL(0) ? InvalidOid : PG_GETARG_OID(0);
RelationSize total_relsize = { 0 };
TupleDesc tupdesc;
HeapTuple tuple;
Datum values[4] = { 0 };
bool nulls[4] = { false };
Cache *hcache;
Hypertable *ht;
ScanIterator iterator = ts_scan_iterator_create(CHUNK, RowExclusiveLock, CurrentMemoryContext);

/* Build a tuple descriptor for our result type */
if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("function returning record called in context "
"that cannot accept type record")));

if (!OidIsValid(relid))
PG_RETURN_NULL();

/* go ahead only if this is a hypertable or a CAgg */
hcache = ts_hypertable_cache_pin();
ht = ts_resolve_hypertable_from_table_or_cagg(hcache, relid, true);
if (ht == NULL)
{
ts_cache_release(hcache);
PG_RETURN_NULL();

Check warning on line 1230 in src/utils.c

View check run for this annotation

Codecov / codecov/patch

src/utils.c#L1229-L1230

Added lines #L1229 - L1230 were not covered by tests
}

/* get the main hypertable size */
total_relsize = ts_relation_approximate_size_impl(relid);

iterator = ts_scan_iterator_create(CHUNK, RowExclusiveLock, CurrentMemoryContext);
init_scan_by_hypertable_id(&iterator, ht->fd.id);
ts_scanner_foreach(&iterator)
{
bool isnull, dropped, is_osm_chunk;
TupleInfo *ti = ts_scan_iterator_tuple_info(&iterator);
Datum id = slot_getattr(ti->slot, Anum_chunk_id, &isnull);
Datum comp_id = DatumGetInt32(slot_getattr(ti->slot, Anum_chunk_id, &isnull));
int32 chunk_id, compressed_chunk_id;
Oid chunk_relid, compressed_chunk_relid;
RelationSize chunk_relsize, compressed_chunk_relsize;

if (isnull)
continue;

/* only consider chunks that are not dropped */
dropped = DatumGetBool(slot_getattr(ti->slot, Anum_chunk_dropped, &isnull));
Assert(!isnull);
if (dropped)
continue;

Check warning on line 1255 in src/utils.c

View check run for this annotation

Codecov / codecov/patch

src/utils.c#L1255

Added line #L1255 was not covered by tests

chunk_id = DatumGetInt32(id);

/* avoid if it's an OSM chunk */
is_osm_chunk = slot_getattr(ti->slot, Anum_chunk_osm_chunk, &isnull);
Assert(!isnull);
if (is_osm_chunk)
continue;

chunk_relid = ts_chunk_get_relid(chunk_id, false);
chunk_relsize = ts_relation_approximate_size_impl(chunk_relid);
/* add this chunk's size to the total size */
ADD_RELATIONSIZE(total_relsize, chunk_relsize);

/* check if the chunk has a compressed counterpart and add if yes */
comp_id = slot_getattr(ti->slot, Anum_chunk_compressed_chunk_id, &isnull);
if (isnull)
continue;

compressed_chunk_id = DatumGetInt32(comp_id);
compressed_chunk_relid = ts_chunk_get_relid(compressed_chunk_id, false);
compressed_chunk_relsize = ts_relation_approximate_size_impl(compressed_chunk_relid);
/* add this compressed chunk's size to the total size */
ADD_RELATIONSIZE(total_relsize, compressed_chunk_relsize);
}
ts_scan_iterator_close(&iterator);

tupdesc = BlessTupleDesc(tupdesc);

values[0] = Int64GetDatum(total_relsize.heap_size);
values[1] = Int64GetDatum(total_relsize.index_size);
values[2] = Int64GetDatum(total_relsize.toast_size);
values[3] = Int64GetDatum(total_relsize.total_size);

tuple = heap_form_tuple(tupdesc, values, nulls);
ts_cache_release(hcache);

return HeapTupleGetDatum(tuple);
}

#define STR_VALUE(str) #str
#define NODE_CASE(name) \
case T_##name: \
Expand Down
Loading

0 comments on commit fa2d99a

Please sign in to comment.