├── .gitignore ├── .gitmodules ├── LICENSE ├── Makefile ├── README.md ├── debian ├── control ├── control.in ├── pgversions ├── rules └── source │ └── format ├── pg_comment_stats--1.0.sql ├── pg_comment_stats.c ├── pg_comment_stats.control ├── pg_comment_stats.h └── pg_comment_stats_constants.h /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | 3 | pg_time_buffer 4 | pg_stat_kcache 5 | 6 | # Prerequisites 7 | *.d 8 | 9 | # Object files 10 | *.o 11 | *.ko 12 | *.obj 13 | *.elf 14 | 15 | # Linker output 16 | *.ilk 17 | *.map 18 | *.exp 19 | 20 | # Precompiled Headers 21 | *.gch 22 | *.pch 23 | 24 | # Libraries 25 | *.lib 26 | *.a 27 | *.la 28 | *.lo 29 | 30 | # Shared objects (inc. Windows DLLs) 31 | *.dll 32 | *.so 33 | *.so.* 34 | *.dylib 35 | 36 | # Executables 37 | *.exe 38 | *.out 39 | *.app 40 | *.i*86 41 | *.x86_64 42 | *.hex 43 | 44 | # Debug files 45 | *.dSYM/ 46 | *.su 47 | *.idb 48 | *.pdb 49 | 50 | # Kernel Module Compile Results 51 | *.mod* 52 | *.cmd 53 | .tmp_versions/ 54 | modules.order 55 | Module.symvers 56 | Mkfile.old 57 | dkms.conf 58 | 59 | *.bc -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "pg_time_buffer"] 2 | path = pg_time_buffer 3 | url = https://github.com/munakoiso/pg_time_buffer.git 4 | [submodule "pg_stat_kcache"] 5 | path = pg_stat_kcache 6 | url = https://github.com/powa-team/pg_stat_kcache.git 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2018, PostgreSQL Global Development Group 2 | 3 | Permission to use, copy, modify, and distribute this software and its 4 | documentation for any purpose, without fee, and without a written agreement 5 | is hereby granted, provided that the above copyright notice and this 6 | paragraph and the following two paragraphs appear in all copies. 7 | 8 | IN NO EVENT SHALL POSTGRESQL GLOBAL DEVELOPMENT GROUP BE LIABLE TO ANY 9 | PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, 10 | INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS 11 | DOCUMENTATION, EVEN IF POSTGRESQL GLOBAL DEVELOPMENT GROUP HAS BEEN 12 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | 14 | POSTGRESQL GLOBAL DEVELOPMENT GROUP SPECIFICALLY DISCLAIMS ANY WARRANTIES, 15 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY 16 | AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS 17 | ON AN "AS IS" BASIS, AND POSTGRESQL GLOBAL DEVELOPMENT GROUP HAS NO 18 | OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR 19 | MODIFICATIONS. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | EXTENSION = pg_comment_stats 2 | EXTVERSION = $(shell grep default_version $(EXTENSION).control | sed -e "s/default_version[[:space:]]*=[[:space:]]*'\([^']*\)'/\1/") 3 | TESTS = $(wildcard test/sql/*.sql) 4 | REGRESS = $(patsubst test/sql/%.sql,%,$(TESTS)) 5 | REGRESS_OPTS = --inputdir=test 6 | 7 | PG_CONFIG ?= pg_config 8 | 9 | MODULE_big = pg_comment_stats 10 | OBJS = pg_comment_stats.o pg_time_buffer/pg_time_buffer.o 11 | 12 | submodules: 13 | git submodule update --init pg_stat_kcache 14 | git submodule update --init pg_time_buffer 15 | 16 | all: submodules 17 | 18 | release-zip: all 19 | git archive --format zip --prefix=pg_comment_stats-${EXTVERSION}/ --output ./pg_comment_stats-${EXTVERSION}.zip HEAD 20 | unzip ./pg_comment_stats-$(EXTVERSION).zip 21 | rm ./pg_comment_stats-$(EXTVERSION).zip 22 | rm ./pg_comment_stats-$(EXTVERSION)/.gitignore 23 | sed -i -e "s/__VERSION__/$(EXTVERSION)/g" ./pg_comment_stats-$(EXTVERSION)/META.json 24 | zip -r ./pg_comment_stats-$(EXTVERSION).zip ./pg_comment_stats-$(EXTVERSION)/ 25 | rm ./pg_comment_stats-$(EXTVERSION) -rf 26 | 27 | 28 | DATA = $(wildcard *--*.sql) 29 | PGXS := $(shell $(PG_CONFIG) --pgxs) 30 | include $(PGXS) 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pg_comment_stats 2 | 3 | Saves stats about queries using keys from comments and counters from pg_stat_kcache. 4 | 5 | Extension requires pg_stat_statements and pg_stat_kcache extension to be installed. 6 | 7 | Compiling 8 | --------- 9 | 10 | The module can be built using the standard PGXS infrastructure. For this to 11 | work, the ``pg_config`` program must be available in your $PATH. 12 | Also, [pg_time_buffer](https://github.com/munakoiso/pg_time_buffer) 13 | and header from [pg_stat_kcache](https://github.com/powa-team/pg_stat_kcache) is required to compile. 14 | 15 | Instruction to 16 | install follows: 17 | ``` 18 | git clone https://github.com/powa-team/pg_stat_kcache.git 19 | git clone https://github.com/munakoiso/pg_time_buffer.git 20 | git clone https://github.com/munakoiso/pg_comment_stats.git 21 | cp pg_stat_kcache/pg_stat_kcache.h pg_comment_stats/ 22 | cp pg_time_buffer/* pg_comment_stats/ 23 | cd pg_comment_stats 24 | make 25 | make install 26 | ``` 27 | 28 | Setup 29 | --- 30 | You must add the module to ``shared_preload_libraries`` in your ``postgresql.conf``. 31 | 32 | 33 | Configuration 34 | --- 35 | The following GUCs can be configured, in ``postgresql.conf``: 36 | 37 | - *pg_comment_stats.buffer_size* - amount of shared memory (in mb), that will be allocated by extension. 38 | - *pg_comment_stats.stat_time_interval* - interval of time (in seconds), during which stats will be stored in the buffer. After this interval, each stat will be removed from the buffer. 39 | - *pg_comment_stats.excluded_keys* - list of excluded keys (for example "first_key,second_key") 40 | 41 | Usage 42 | ----- 43 | 44 | 45 | Add comments like this to your queries: 46 | 47 | ```> /* a: 1 b: qwerty*/ some_query;``` 48 | 49 | Select stats about specified keys from all buffer: 50 | ``` 51 | > select * from pgcs_get_stats(); 52 | NOTICE: 00000: pgcs: Show stats from '2021-09-29 15:33:03.97336+03' to '2021-09-29 15:33:13.785069+03' 53 | LOCATION: pgcs_internal_get_stats_time_interval, pg_comment_stats.c:848 54 | -[ RECORD 1 ]+--------------------------- 55 | comment_keys | {"a": "1", "b": "qwerty"} 56 | query_count | 1 57 | userid | 10 58 | dbid | 14409 59 | reads | 0 60 | writes | 0 61 | user_time | 5.700000000000019e-05 62 | system_time | 0 63 | minflts | 7 64 | majflts | 0 65 | nswaps | 0 66 | msgsnds | 0 67 | msgrcvs | 0 68 | nsignals | 0 69 | nvcsws | 0 70 | nivcsws | 0 71 | 72 | Time: 3.649 ms 73 | ``` 74 | 75 | Or from specific time interval (with accuracy 1% of stat_time_interval): 76 | 77 | ``` 78 | > select * from pgcs_get_stats_time_interval(now() - '30 s'::interval, now()); 79 | NOTICE: 00000: pgcs: Show stats from '2021-09-29 15:36:08.98296+03' to '2021-09-29 15:36:39.567835+03' 80 | LOCATION: pgcs_internal_get_stats_time_interval, pg_comment_stats.c:848 81 | -[ RECORD 1 ]+--------------------------- 82 | comment_keys | {"a": "1", "b": "qwerty"} 83 | query_count | 1 84 | userid | 10 85 | dbid | 14409 86 | reads | 0 87 | writes | 0 88 | user_time | 0 89 | system_time | 4.0000000000000105e-05 90 | minflts | 0 91 | majflts | 0 92 | nswaps | 0 93 | msgsnds | 0 94 | msgrcvs | 0 95 | nsignals | 0 96 | nvcsws | 0 97 | nivcsws | 0 98 | 99 | Time: 1.408 m 100 | ``` 101 | 102 | If needed, you can exclude some keys: 103 | 104 | ``` 105 | > select pgcs_exclude_key('c'); 106 | pgcs_exclude_key 107 | ------------------ 108 | 109 | (1 row) 110 | 111 | Time: 0.344 ms 112 | 113 | > /* a: 1 c: hmm*/ select 1; 114 | ?column? 115 | ---------- 116 | 1 117 | (1 row) 118 | 119 | Time: 0.209 ms 120 | 121 | > select * from pgcs_get_stats() limit 1; 122 | NOTICE: 00000: pgcs: Show stats from '2021-09-29 15:37:54.982949+03' to '2021-09-29 15:39:36.513423+03' 123 | LOCATION: pgcs_internal_get_stats_time_interval, pg_comment_stats.c:848 124 | -[ RECORD 1 ]+---------------------- 125 | comment_keys | {"a": "1"} 126 | query_count | 1 127 | userid | 10 128 | dbid | 14409 129 | reads | 0 130 | writes | 0 131 | user_time | 6.000000000000363e-06 132 | system_time | 1.299999999999999e-05 133 | minflts | 0 134 | majflts | 0 135 | nswaps | 0 136 | msgsnds | 0 137 | msgrcvs | 0 138 | nsignals | 0 139 | nvcsws | 0 140 | nivcsws | 0 141 | 142 | Time: 1.125 ms 143 | ``` 144 | 145 | Select or reset excluded keys: 146 | 147 | ``` 148 | > select * from pgcs_get_excluded_keys(); 149 | excluded_key 150 | -------------- 151 | c 152 | (1 row) 153 | 154 | Time: 0.283 ms 155 | > select * from pgcs_reset_excluded_keys(); 156 | pgcs_reset_excluded_keys 157 | -------------------------- 158 | 159 | (1 row) 160 | 161 | Time: 0.285 ms 162 | > select * from pgcs_get_excluded_keys(); 163 | excluded_key 164 | -------------- 165 | (0 rows) 166 | 167 | Time: 0.188 ms 168 | ``` -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: pg-comment-stats 2 | Section: database 3 | Priority: optional 4 | Maintainer: munakoiso 5 | Standards-Version: 4.6.2 6 | Rules-Requires-Root: no 7 | Build-Depends: debhelper-compat (= 13), postgresql-all (>= 217~) 8 | 9 | Package: postgresql-16-pg-comment-stats 10 | Architecture: any 11 | Depends: ${misc:Depends}, ${shlibs:Depends}, ${postgresql:Depends}, 12 | postgresql-contrib-16 13 | Description: PostgreSQL extension that saves stats about queries using keys from comments and counters from pg_stat_kcache 14 | 15 | 16 | Package: postgresql-15-pg-comment-stats 17 | Architecture: any 18 | Depends: ${misc:Depends}, ${shlibs:Depends}, ${postgresql:Depends}, 19 | postgresql-contrib-15 20 | Description: PostgreSQL extension that saves stats about queries using keys from comments and counters from pg_stat_kcache. 21 | 22 | 23 | Package: postgresql-14-pg-comment-stats 24 | Architecture: any 25 | Depends: ${misc:Depends}, ${shlibs:Depends}, ${postgresql:Depends}, 26 | postgresql-contrib-14 27 | Description: PostgreSQL extension that saves stats about queries using keys from comments and counters from pg_stat_kcache. 28 | 29 | 30 | Package: postgresql-13-pg-comment-stats 31 | Architecture: any 32 | Depends: ${misc:Depends}, ${shlibs:Depends}, ${postgresql:Depends}, 33 | postgresql-contrib-13 34 | Description: PostgreSQL extension that saves stats about queries using keys from comments and counters from pg_stat_kcache. 35 | 36 | 37 | Package: postgresql-12-pg-comment-stats 38 | Architecture: any 39 | Depends: ${misc:Depends}, ${shlibs:Depends}, ${postgresql:Depends}, 40 | postgresql-contrib-12 41 | Description: PostgreSQL extension that saves stats about queries using keys from comments and counters from pg_stat_kcache. 42 | 43 | 44 | Package: postgresql-11-pg-comment-stats 45 | Architecture: any 46 | Depends: ${misc:Depends}, ${shlibs:Depends}, ${postgresql:Depends}, 47 | postgresql-contrib-11 48 | Description: PostgreSQL extension that saves stats about queries using keys from comments and counters from pg_stat_kcache. 49 | -------------------------------------------------------------------------------- /debian/control.in: -------------------------------------------------------------------------------- 1 | Source: pg-comment-stats 2 | Section: database 3 | Priority: optional 4 | Maintainer: munakoiso 5 | Standards-Version: 4.6.2 6 | Rules-Requires-Root: no 7 | Build-Depends: debhelper-compat (= 13), postgresql-all (>= 217~) 8 | 9 | Package: postgresql-PGVERSION-pg-comment-stats 10 | Architecture: any 11 | Depends: ${misc:Depends}, ${shlibs:Depends}, ${postgresql:Depends}, 12 | postgresql-contrib-PGVERSION 13 | Description: PostgreSQL extension that saves stats about queries using keys from comments and counters from pg_stat_kcache. 14 | -------------------------------------------------------------------------------- /debian/pgversions: -------------------------------------------------------------------------------- 1 | 11 2 | 12 3 | 13 4 | 14 5 | 15 6 | 16 7 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | %: 4 | dh $@ --with pgxs_loop 5 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (native) 2 | -------------------------------------------------------------------------------- /pg_comment_stats--1.0.sql: -------------------------------------------------------------------------------- 1 | CREATE FUNCTION pgcs_get_stats( 2 | OUT comment_keys jsonb, 3 | OUT query_count integer, 4 | OUT userid oid, 5 | OUT dbid oid, 6 | OUT reads bigint, /* total reads, in bytes */ 7 | OUT writes bigint, /* total writes, in bytes */ 8 | OUT user_time double precision, /* total user CPU time used */ 9 | OUT system_time double precision, /* total system CPU time used */ 10 | OUT minflts bigint, /* total page reclaims (soft page faults) */ 11 | OUT majflts bigint, /* total page faults (hard page faults) */ 12 | OUT nswaps bigint, /* total swaps */ 13 | OUT msgsnds bigint, /* total IPC messages sent */ 14 | OUT msgrcvs bigint, /* total IPC messages received */ 15 | OUT nsignals bigint, /* total signals received */ 16 | OUT nvcsws bigint, /* total voluntary context switches */ 17 | OUT nivcsws bigint /* total involuntary context switches */ 18 | ) 19 | RETURNS SETOF record 20 | AS 'MODULE_PATHNAME', 'pgcs_get_stats' 21 | LANGUAGE C STRICT; 22 | 23 | CREATE FUNCTION pgcs_get_stats_time_interval( 24 | start_ts timestamptz, 25 | stop_ts timestamptz, 26 | OUT comment_keys jsonb, 27 | OUT query_count integer, 28 | OUT userid oid, 29 | OUT dbid oid, 30 | OUT reads bigint, /* total reads, in bytes */ 31 | OUT writes bigint, /* total writes, in bytes */ 32 | OUT user_time double precision, /* total user CPU time used */ 33 | OUT system_time double precision, /* total system CPU time used */ 34 | OUT minflts bigint, /* total page reclaims (soft page faults) */ 35 | OUT majflts bigint, /* total page faults (hard page faults) */ 36 | OUT nswaps bigint, /* total swaps */ 37 | OUT msgsnds bigint, /* total IPC messages sent */ 38 | OUT msgrcvs bigint, /* total IPC messages received */ 39 | OUT nsignals bigint, /* total signals received */ 40 | OUT nvcsws bigint, /* total voluntary context switches */ 41 | OUT nivcsws bigint /* total involuntary context switches */ 42 | ) 43 | RETURNS SETOF record 44 | AS 'MODULE_PATHNAME', 'pgcs_get_stats_time_interval' 45 | LANGUAGE C STRICT; 46 | 47 | CREATE FUNCTION pgcs_exclude_key( 48 | exclude_string cstring 49 | ) 50 | RETURNS void 51 | AS 'MODULE_PATHNAME', 'pgcs_exclude_key' 52 | LANGUAGE C STRICT; 53 | 54 | CREATE FUNCTION pgcs_get_excluded_keys( 55 | OUT excluded_key text 56 | ) 57 | RETURNS SETOF text 58 | AS 'MODULE_PATHNAME', 'pgcs_get_excluded_keys' 59 | LANGUAGE C STRICT; 60 | 61 | CREATE FUNCTION pgcs_reset_excluded_keys() 62 | RETURNS void 63 | AS 'MODULE_PATHNAME', 'pgcs_reset_excluded_keys' 64 | LANGUAGE C STRICT; 65 | 66 | CREATE FUNCTION pgcs_get_buffer_stats( 67 | OUT saved_strings_count integer, 68 | OUT available_strings_count integer 69 | ) 70 | RETURNS SETOF record 71 | AS 'MODULE_PATHNAME', 'pgcs_get_buffer_stats' 72 | LANGUAGE C STRICT; 73 | -------------------------------------------------------------------------------- /pg_comment_stats.c: -------------------------------------------------------------------------------- 1 | #include "postgres.h" 2 | 3 | #include 4 | #include "access/tupdesc.h" 5 | #include "access/htup_details.h" 6 | #include "lib/stringinfo.h" 7 | #include "postmaster/bgworker.h" 8 | #include "storage/lwlock.h" 9 | #include "storage/proc.h" 10 | #include "storage/shmem.h" 11 | #include "utils/datetime.h" 12 | #include "utils/dynahash.h" 13 | #include "utils/timestamp.h" 14 | #include "pg_comment_stats.h" 15 | #include "utils/guc.h" 16 | 17 | PG_MODULE_MAGIC; 18 | 19 | static char *worker_name = "pgcs_worker"; 20 | 21 | static char *extension_name = "pg_comment_stats"; 22 | 23 | static HTAB *string_to_id = NULL; 24 | 25 | static HTAB *id_to_string = NULL; 26 | 27 | static GlobalInfo *global_variables = NULL; 28 | 29 | static volatile sig_atomic_t got_sigterm = false; 30 | 31 | static int stat_time_interval; 32 | static int buffer_size_mb; 33 | static Size buffer_size; 34 | static char* excluded_keys = NULL; 35 | 36 | void _PG_init(void); 37 | void _PG_fini(void); 38 | 39 | static pgsk_counters_hook_type prev_pgsk_counters_hook = NULL; 40 | static shmem_startup_hook_type prev_shmem_startup_hook = NULL; 41 | 42 | #if (PG_VERSION_NUM >= 150000) 43 | static shmem_request_hook_type prev_shmem_request_hook = NULL; 44 | static void pgcs_shmem_request(void); 45 | #endif 46 | 47 | static void pgcs_shmem_startup(void); 48 | void pgcs_register_bgworker(void); 49 | static void get_composite_key_by_query(char*, pgcsCompositeKey*, bool*); 50 | static int get_index_of_comment_key(char*); 51 | static int get_index_of_comment_key_lock_mode(char*, bool); 52 | static int get_id_from_string(char*); 53 | static Datum pgcs_internal_get_stats_time_interval(TimestampTz, TimestampTz, FunctionCallInfo); 54 | void pgcs_add(void*, void*); 55 | void pgcs_on_delete(void*, void*); 56 | static uint64_t pgcs_find_optimal_memory_split(uint64_t, uint64_t, uint64_t*, uint64_t*, uint64_t*, int*, int*); 57 | void pg_comment_stats_main(Datum main_arg); 58 | void pgcs_store_aggregated_counters(pgskCounters*, const char*, int, pgskStoreKind); 59 | 60 | static int 61 | get_random_int(void) 62 | { 63 | int x; 64 | 65 | x = (int) (random() & 0xFFFF) << 16; 66 | x |= (int) (random() & 0xFFFF); 67 | return x; 68 | } 69 | 70 | static void 71 | pg_comment_stats_sigterm(SIGNAL_ARGS) { 72 | int save_errno = errno; 73 | got_sigterm = true; 74 | if (MyProc) 75 | SetLatch(&MyProc->procLatch); 76 | errno = save_errno; 77 | } 78 | 79 | void 80 | _PG_init(void) { 81 | if (!process_shared_preload_libraries_in_progress) { 82 | elog(ERROR, "This module can only be loaded via shared_preload_libraries"); 83 | return; 84 | } 85 | 86 | pgcs_register_bgworker(); 87 | DefineCustomIntVariable("pg_comment_stats.buffer_size", 88 | "Max amount of shared memory (in megabytes), that can be allocated", 89 | "Default of 20, max of 5000", 90 | &buffer_size_mb, 91 | 20, 92 | min_buffer_size_mb, 93 | max_buffer_size_mb, 94 | PGC_SUSET, 95 | GUC_UNIT_MB | GUC_NO_RESET_ALL, 96 | NULL, 97 | NULL, 98 | NULL); 99 | 100 | DefineCustomIntVariable("pg_comment_stats.stat_time_interval", 101 | "Duration of stat collecting interval", 102 | "Default of 6000s, max of 360000s", 103 | &stat_time_interval, 104 | 6000, 105 | min_time_interval, 106 | max_time_interval, 107 | PGC_SUSET, 108 | GUC_UNIT_S | GUC_NO_RESET_ALL, 109 | NULL, 110 | NULL, 111 | NULL); 112 | 113 | DefineCustomStringVariable("pg_comment_stats.excluded_keys", 114 | "Excluded keys separated by ','", 115 | NULL, 116 | &excluded_keys, 117 | NULL, 118 | PGC_POSTMASTER, 119 | 0, /* no flags required */ 120 | NULL, 121 | NULL, 122 | NULL); 123 | 124 | DefineCustomBoolVariable("pg_comment_stats.enabled", 125 | "Flag to enable or disable stats collecting.", 126 | NULL, 127 | &pgcs_enabled, 128 | true, 129 | PGC_SIGHUP, 130 | 0, /* no flags required */ 131 | NULL, 132 | NULL, 133 | NULL); 134 | 135 | buffer_size = ((Size)buffer_size_mb) * 1024 * 1024; 136 | 137 | EmitWarningsOnPlaceholders("pg_comment_stats"); 138 | 139 | #if (PG_VERSION_NUM >= 150000) 140 | prev_shmem_request_hook = shmem_request_hook; 141 | shmem_request_hook = pgcs_shmem_request; 142 | #else 143 | RequestAddinShmemSpace(CACHELINEALIGN(buffer_size)); 144 | #endif 145 | 146 | #if (PG_VERSION_NUM < 150000) 147 | #if PG_VERSION_NUM >= 90500 148 | RequestNamedLWLockTranche("pg_comment_stats", 1); 149 | #else 150 | RequestAddinLWLocks(1); 151 | #endif 152 | #endif 153 | 154 | /* Install hook */ 155 | prev_pgsk_counters_hook = pgsk_counters_hook; 156 | pgsk_counters_hook = pgcs_store_aggregated_counters; 157 | prev_shmem_startup_hook = shmem_startup_hook; 158 | shmem_startup_hook = pgcs_shmem_startup; 159 | } 160 | 161 | void 162 | _PG_fini(void) 163 | { 164 | /* uninstall hook */ 165 | shmem_startup_hook = prev_shmem_startup_hook; 166 | pgsk_counters_hook = prev_pgsk_counters_hook; 167 | } 168 | 169 | 170 | #if (PG_VERSION_NUM >= 150000) 171 | /* 172 | * Requests any additional shared memory required for our extension 173 | */ 174 | static void 175 | pgcs_shmem_request(void) 176 | { 177 | if (prev_shmem_request_hook) 178 | prev_shmem_request_hook(); 179 | 180 | RequestAddinShmemSpace(CACHELINEALIGN(buffer_size)); 181 | RequestNamedLWLockTranche("pg_comment_stats", 1); 182 | } 183 | #endif 184 | 185 | static void 186 | pgcs_shmem_startup(void) 187 | { 188 | bool found; 189 | HASHCTL info; 190 | bool found_global_info; 191 | int id; 192 | int items_count; 193 | pgcsStringFromId *stringFromId; 194 | uint64_t total_shmem; 195 | uint64_t pgtb_size; 196 | int global_var_const_size; 197 | uint64_t string_to_id_htab_item; 198 | uint64_t id_to_string_htab_item; 199 | char excluded_keys_copy[max_parameters_count * max_parameter_length]; 200 | char* excluded_key; 201 | 202 | if (prev_shmem_startup_hook) 203 | prev_shmem_startup_hook(); 204 | 205 | /* Create or attach to the shared memory state */ 206 | LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE); 207 | 208 | global_var_const_size = sizeof(GlobalInfo); 209 | string_to_id_htab_item = (sizeof(char) * max_parameter_length + sizeof(pgcsIdFromString)); 210 | id_to_string_htab_item = (sizeof(int) + sizeof(pgcsStringFromId)); 211 | 212 | total_shmem = buffer_size_mb * 1024 * 1024; 213 | pgtb_size = pgcs_find_optimal_memory_split(0, 214 | total_shmem, 215 | &string_to_id_htab_item, 216 | &id_to_string_htab_item, 217 | &total_shmem, 218 | &global_var_const_size, 219 | &items_count); 220 | 221 | pgtb_init(extension_name, 222 | &pgcs_add, 223 | &pgcs_on_delete, 224 | (int)(stat_time_interval / buckets_count + 0.5), 225 | pgtb_size, 226 | sizeof(pgcsBucketItem), 227 | sizeof(pgskCounters)); 228 | 229 | global_variables = ShmemInitStruct("pg_comment_stats global_variables", 230 | sizeof(GlobalInfo), 231 | &found_global_info); 232 | 233 | global_variables->bucket_duration = (int)(stat_time_interval / buckets_count + 0.5); 234 | global_variables->items_count = items_count; 235 | elog(LOG, "pgcs: Max count of unique strings: %d", global_variables->items_count); 236 | 237 | memset(&info, 0, sizeof(info)); 238 | info.keysize = sizeof(char) * max_parameter_length; 239 | info.entrysize = sizeof(pgcsIdFromString); 240 | string_to_id = ShmemInitHash("pg_comment_stats string_to_id_htab", 241 | items_count, items_count, 242 | &info, 243 | HASH_ELEM | HASH_BLOBS); 244 | 245 | memset(&info, 0, sizeof(info)); 246 | info.keysize = sizeof(int); 247 | info.entrysize = sizeof(pgcsStringFromId); 248 | 249 | id_to_string = ShmemInitHash("pg_comment_stats id_to_string_htab", 250 | items_count, items_count, 251 | &info, 252 | HASH_ELEM | HASH_BLOBS); 253 | 254 | id = comment_value_not_specified; 255 | stringFromId = hash_search(id_to_string, (void *) &id, HASH_ENTER, &found); 256 | global_variables->currents_strings_count = 1; 257 | stringFromId->id = id; 258 | SpinLockInit(&stringFromId->mutex); 259 | memset(stringFromId->string, '\0', sizeof(stringFromId->string)); 260 | 261 | memset(&global_variables->excluded_keys, '\0', sizeof(global_variables->excluded_keys)); 262 | global_variables->excluded_keys_count = 0; 263 | 264 | if (excluded_keys != NULL) { 265 | /* make sure that variable not too big */ 266 | memset(&excluded_keys_copy, '\0', sizeof(excluded_keys_copy)); 267 | strlcpy(&excluded_keys_copy[0], excluded_keys, max_parameters_count * max_parameter_length); 268 | excluded_key = strtok(excluded_keys_copy, ","); 269 | while (excluded_key != NULL) { 270 | strlcpy(&global_variables->excluded_keys[global_variables->excluded_keys_count][0], excluded_key, max_parameter_length - 1); 271 | global_variables->excluded_keys_count += 1; 272 | 273 | excluded_key = strtok(NULL, " "); 274 | } 275 | } 276 | LWLockRelease(AddinShmemInitLock); 277 | } 278 | 279 | static bool 280 | is_key_excluded(char *key) { 281 | int i; 282 | for (i = 0; i < global_variables->excluded_keys_count; ++i) { 283 | if (strcmp(key, global_variables->excluded_keys[i]) == 0) 284 | return true; 285 | } 286 | return false; 287 | } 288 | 289 | static int 290 | get_index_of_comment_key(char *key) { 291 | int key_index; 292 | 293 | key_index = get_index_of_comment_key_lock_mode(key, false); 294 | if (key_index == comment_key_not_specified) { 295 | /* Now read all comment keys with write lock. If there is still no match - create one */ 296 | key_index = get_index_of_comment_key_lock_mode(key, true); 297 | } 298 | return key_index; 299 | } 300 | 301 | static int 302 | get_index_of_comment_key_lock_mode(char *key, bool is_exclusive) { 303 | int i; 304 | if (global_variables == NULL) { 305 | return comment_key_not_specified; 306 | } 307 | 308 | if (is_exclusive) { 309 | LWLockAcquire(&global_variables->lock, LW_EXCLUSIVE); 310 | } else { 311 | LWLockAcquire(&global_variables->lock, LW_SHARED); 312 | } 313 | if (is_key_excluded(key)) { 314 | LWLockRelease(&global_variables->lock); 315 | return comment_key_not_specified; 316 | } 317 | 318 | for (i = 0; i < global_variables->keys_count; ++i) { 319 | if (strcmp(key, global_variables->commentKeys[i]) == 0) { 320 | LWLockRelease(&global_variables->lock); 321 | return i; 322 | } 323 | } 324 | if (!is_exclusive) { 325 | LWLockRelease(&global_variables->lock); 326 | return comment_key_not_specified; 327 | } 328 | 329 | if (global_variables->keys_count == max_parameters_count) { 330 | global_variables->keys_overflow = true; 331 | LWLockRelease(&global_variables->lock); 332 | return comment_key_not_specified; 333 | } 334 | strcpy(global_variables->commentKeys[global_variables->keys_count++], key); 335 | i = global_variables->keys_count - 1; 336 | LWLockRelease(&global_variables->lock); 337 | 338 | return i; 339 | } 340 | 341 | static void 342 | get_composite_key_by_query(char *query, pgcsCompositeKey *compositeKey, bool *is_comment_exist) { 343 | char query_copy[(max_parameter_length + 2) * max_parameters_count * 2]; 344 | char *end_of_comment; 345 | char *start_of_comment; 346 | int comment_length; 347 | int len; 348 | int i; 349 | char end_of_key[2] = ":\0"; 350 | int key_index = -1; 351 | char *query_prefix; 352 | int value_id; 353 | 354 | start_of_comment = strstr(query, "/*"); 355 | end_of_comment = strstr(query, "*/"); 356 | 357 | *is_comment_exist = false; 358 | if (start_of_comment == NULL || end_of_comment == NULL) { 359 | compositeKey = NULL; 360 | return; 361 | } 362 | 363 | start_of_comment += 3; 364 | comment_length = end_of_comment - start_of_comment; 365 | memset(&query_copy, '\0', sizeof(query_copy)); 366 | strlcpy(query_copy, start_of_comment, comment_length + 1); 367 | query_copy[comment_length] = '\0'; 368 | query_prefix = strtok(query_copy, " "); 369 | 370 | for (i = 0; i < max_parameters_count; ++i) { 371 | compositeKey->keyValues[i] = comment_value_not_specified; 372 | } 373 | while (query_prefix != NULL) { 374 | len = strlen(query_prefix); 375 | if (strcmp(query_prefix + len - 1, end_of_key) == 0 && key_index == -1) { 376 | /* it's key */ 377 | query_prefix[len - 1] = '\0'; 378 | key_index = get_index_of_comment_key(query_prefix); 379 | } else { 380 | /* it's value */ 381 | if (key_index == comment_key_not_specified) { 382 | query_prefix = strtok(NULL, " "); 383 | continue; 384 | } 385 | value_id = get_id_from_string(query_prefix); 386 | if (value_id == -1) { 387 | return; 388 | } 389 | *is_comment_exist = true; 390 | compositeKey->keyValues[key_index] = value_id; 391 | key_index = -1; 392 | } 393 | 394 | query_prefix = strtok(NULL, " "); 395 | } 396 | if (key_index != -1) 397 | elog(WARNING, "pg_comment_stats: incorrect comment"); 398 | } 399 | 400 | static uint64_t 401 | pgcs_find_optimal_items_count(uint64_t left_bound, 402 | uint64_t right_bound, 403 | uint64_t* string_to_id_htab_item, 404 | uint64_t* id_to_string_htab_item, 405 | uint64_t* total_size) { 406 | uint64_t string_to_id_htab_size; 407 | uint64_t id_to_string_htab_size; 408 | uint64_t middle; 409 | 410 | middle = (right_bound + left_bound) / 2; 411 | string_to_id_htab_size = hash_estimate_size(middle, *string_to_id_htab_item); 412 | id_to_string_htab_size = hash_estimate_size(middle, *id_to_string_htab_item); 413 | 414 | if (left_bound + 1 == right_bound) { 415 | return left_bound; 416 | } 417 | 418 | if (string_to_id_htab_size + id_to_string_htab_size > *total_size) { 419 | return pgcs_find_optimal_items_count(left_bound, 420 | middle, 421 | string_to_id_htab_item, 422 | id_to_string_htab_item, 423 | total_size); 424 | } else { 425 | return pgcs_find_optimal_items_count(middle, 426 | right_bound, 427 | string_to_id_htab_item, 428 | id_to_string_htab_item, 429 | total_size); 430 | } 431 | } 432 | 433 | static uint64_t 434 | pgcs_find_optimal_memory_split(uint64_t left_bound, 435 | uint64_t right_bound, 436 | uint64_t* string_to_id_htab_item, 437 | uint64_t* id_to_string_htab_item, 438 | uint64_t* total_size, 439 | int* global_var_const_size, 440 | int* pgcs_items) { 441 | int pgcs_max_items_count; 442 | uint64_t middle; 443 | uint64_t pgcs_size; 444 | int pgtb_items; 445 | 446 | middle = (right_bound + left_bound) / 2; 447 | pgcs_size = *total_size - middle; 448 | pgcs_max_items_count = pgcs_size / (*string_to_id_htab_item + *id_to_string_htab_item); 449 | 450 | pgtb_items = pgtb_get_items_count(middle, 451 | sizeof(pgcsBucketItem), 452 | sizeof(pgskCounters)); 453 | 454 | *pgcs_items = pgcs_find_optimal_items_count(0, 455 | pgcs_max_items_count, 456 | string_to_id_htab_item, 457 | id_to_string_htab_item, 458 | &pgcs_size); 459 | 460 | if (left_bound + 1 == right_bound) { 461 | return left_bound; 462 | } 463 | 464 | if (pgtb_items < *pgcs_items) { 465 | return pgcs_find_optimal_memory_split(middle, 466 | right_bound, 467 | string_to_id_htab_item, 468 | id_to_string_htab_item, 469 | total_size, 470 | global_var_const_size, 471 | pgcs_items); 472 | } else { 473 | return pgcs_find_optimal_memory_split(left_bound, 474 | middle, 475 | string_to_id_htab_item, 476 | id_to_string_htab_item, 477 | total_size, 478 | global_var_const_size, 479 | pgcs_items); 480 | } 481 | } 482 | 483 | void 484 | pgcs_store_aggregated_counters(pgskCounters* counters, const char* query_string, int level, pgskStoreKind kind) { 485 | pgcsBucketItem key; 486 | pgskCounters additional_counters; 487 | bool is_comment_exist; 488 | bool stored; 489 | char query[(max_parameter_length + 1) * max_parameters_count]; 490 | 491 | if (global_variables == NULL) { 492 | return; 493 | } 494 | 495 | if (!pgcs_enabled) { 496 | return; 497 | } 498 | 499 | if (prev_pgsk_counters_hook) 500 | prev_pgsk_counters_hook(counters, query_string, level, kind); 501 | 502 | /* Now track only top-level statements */ 503 | /* TODO: maybe add support of other levels? */ 504 | if (level != 0) 505 | return; 506 | 507 | memset(&key.compositeKey, 0, sizeof(pgcsCompositeKey)); 508 | strlcpy(query, query_string, sizeof(query)); 509 | query[sizeof(query) - 1] = '\0'; 510 | get_composite_key_by_query((char*)&query, &key.compositeKey, &is_comment_exist); 511 | if (!is_comment_exist) { 512 | return; 513 | } 514 | key.database = MyDatabaseId; 515 | key.user = GetUserId(); 516 | 517 | additional_counters.usage = 1; 518 | additional_counters.utime = counters->utime; 519 | additional_counters.stime = counters->stime; 520 | 521 | #ifdef HAVE_GETRUSAGE 522 | additional_counters.minflts = counters->minflts; 523 | additional_counters.majflts = counters->majflts; 524 | additional_counters.nswaps = counters->nswaps; 525 | additional_counters.reads = counters->reads; 526 | additional_counters.writes = counters->writes; 527 | additional_counters.msgsnds = counters->msgsnds; 528 | additional_counters.msgrcvs = counters->msgrcvs; 529 | additional_counters.nsignals = counters->nsignals; 530 | additional_counters.nvcsws = counters->nvcsws; 531 | additional_counters.nivcsws = counters->nivcsws; 532 | #endif 533 | stored = pgtb_put(extension_name, &key, &additional_counters); 534 | if (!stored) { 535 | pgcs_on_delete((void*) &key, (void*) &additional_counters); 536 | } 537 | } 538 | 539 | void pgcs_add(void* value, void* anotherValue) { 540 | pgskCounters* counters; 541 | pgskCounters* another_counters; 542 | 543 | counters = (pgskCounters*) value; 544 | another_counters = (pgskCounters*) anotherValue; 545 | 546 | counters->usage += another_counters->usage; 547 | counters->utime += another_counters->utime; 548 | counters->stime += another_counters->stime; 549 | counters->minflts += another_counters->minflts; 550 | counters->majflts += another_counters->majflts; 551 | counters->nswaps += another_counters->nswaps; 552 | counters->reads += another_counters->reads; 553 | counters->writes += another_counters->writes; 554 | counters->msgsnds += another_counters->msgsnds; 555 | counters->msgrcvs += another_counters->msgrcvs; 556 | counters->nsignals += another_counters->nsignals; 557 | counters->nvcsws += another_counters->nvcsws; 558 | counters->nivcsws += another_counters->nivcsws; 559 | } 560 | 561 | void 562 | pgcs_register_bgworker() { 563 | BackgroundWorker worker; 564 | MemSet(&worker, 0, sizeof(BackgroundWorker)); 565 | worker.bgw_flags = BGWORKER_SHMEM_ACCESS; 566 | worker.bgw_start_time = BgWorkerStart_PostmasterStart; 567 | snprintf(worker.bgw_name, BGW_MAXLEN, "%s", worker_name); 568 | sprintf(worker.bgw_library_name, "pg_comment_stats"); 569 | sprintf(worker.bgw_function_name, "pg_comment_stats_main"); 570 | /* Wait 10 seconds for restart after crash */ 571 | worker.bgw_restart_time = 10; 572 | worker.bgw_main_arg = (Datum) 0; 573 | worker.bgw_notify_pid = 0; 574 | RegisterBackgroundWorker(&worker); 575 | } 576 | 577 | static int 578 | get_id_from_string(char *string_pointer) { 579 | char string[max_parameter_length]; 580 | pgcsIdFromString *idFromString; 581 | pgcsStringFromId *stringFromId; 582 | int id; 583 | bool found; 584 | if (!string_to_id || !id_to_string) 585 | return comment_value_not_specified; 586 | if (strlen(string_pointer) >= max_parameter_length) { 587 | elog(WARNING, "pg_comment_stats: Comment value %s too long to store. Max length is %d", 588 | string_pointer, 589 | max_parameter_length); 590 | return comment_value_not_specified; 591 | } 592 | memset(&string, '\0', max_parameter_length); 593 | strcpy(string, string_pointer); 594 | LWLockAcquire(&global_variables->lock, LW_SHARED); 595 | 596 | idFromString = hash_search(string_to_id, (void *) &string, HASH_FIND, &found); 597 | if (found) { 598 | stringFromId = hash_search(id_to_string, (void *) &idFromString->id, HASH_FIND, &found); 599 | } else { 600 | if (global_variables->currents_strings_count >= global_variables->items_count) { 601 | SpinLockAcquire(&global_variables->overflow_mutex); 602 | if (!global_variables->max_strings_count_achieved) { 603 | elog(WARNING, 604 | "pg_comment_stats: Can't handle request. No more memory for save strings are available. " 605 | "Current max count of unique strings = %d. \n" 606 | "Decide to tune pg_comment_stats.buffer_size. ", global_variables->items_count); 607 | } 608 | global_variables->max_strings_count_achieved = true; 609 | global_variables->strings_overflow_by += 1; 610 | SpinLockRelease(&global_variables->overflow_mutex); 611 | LWLockRelease(&global_variables->lock); 612 | return comment_value_not_specified; 613 | } 614 | LWLockRelease(&global_variables->lock); 615 | LWLockAcquire(&global_variables->lock, LW_EXCLUSIVE); 616 | /* To prevent race we should */ 617 | /* check once more, that string still does not exist */ 618 | idFromString = hash_search(string_to_id, (void *) &string, HASH_FIND, &found); 619 | if (found) { 620 | /* If found - we can simply increase counter for string */ 621 | stringFromId = hash_search(id_to_string, (void *) &idFromString->id, HASH_FIND, &found); 622 | } else { 623 | /* If not found - insert string into hash tables */ 624 | /* Generate id that not used yet. */ 625 | found = true; 626 | while (found) { 627 | id = get_random_int(); 628 | stringFromId = hash_search(id_to_string, (void *) &id, HASH_FIND, &found); 629 | } 630 | 631 | stringFromId = hash_search(id_to_string, (void *) &id, HASH_ENTER_NULL, &found); 632 | if (stringFromId == NULL) { 633 | global_variables->out_of_shared_memory = true; 634 | LWLockRelease(&global_variables->lock); 635 | return comment_value_not_specified; 636 | } 637 | global_variables->currents_strings_count += 1; 638 | stringFromId->id = id; 639 | memset(stringFromId->string, '\0', max_parameter_length); 640 | strcpy(stringFromId->string, string); 641 | stringFromId->counter = 0; 642 | SpinLockInit(&stringFromId->mutex); 643 | 644 | idFromString = hash_search(string_to_id, (void *) &string, HASH_ENTER_NULL, &found); 645 | if (idFromString == NULL) { 646 | global_variables->currents_strings_count -= 1; 647 | stringFromId = hash_search(id_to_string, (void *) &id, HASH_REMOVE, &found); 648 | global_variables->out_of_shared_memory = true; 649 | LWLockRelease(&global_variables->lock); 650 | return comment_value_not_specified; 651 | } 652 | memset(idFromString->string, '\0', max_parameter_length); 653 | strcpy(idFromString->string, string); 654 | idFromString->id = id; 655 | } 656 | } 657 | SpinLockAcquire(&stringFromId->mutex); 658 | stringFromId->counter++; 659 | SpinLockRelease(&stringFromId->mutex); 660 | id = idFromString->id; 661 | LWLockRelease(&global_variables->lock); 662 | return id; 663 | } 664 | 665 | static void 666 | get_string_from_id(int id, char* string) { 667 | pgcsStringFromId *stringFromId; 668 | bool found; 669 | if (!string_to_id || !id_to_string) { 670 | return; 671 | } 672 | stringFromId = hash_search(id_to_string, (void *) &id, HASH_FIND, &found); 673 | strlcpy(string, stringFromId->string, max_parameter_length); 674 | } 675 | 676 | void pgcs_on_delete(void* key, void* value) { 677 | pgcsBucketItem* item; 678 | pgskCounters* counters; 679 | pgcsStringFromId *string_struct; 680 | int id; 681 | int i; 682 | bool found; 683 | 684 | if (global_variables == NULL) 685 | return; 686 | 687 | LWLockAcquire(&global_variables->lock, LW_EXCLUSIVE); 688 | item = (pgcsBucketItem*) key; 689 | counters = (pgskCounters*) value; 690 | 691 | for (i = 0; i < global_variables->keys_count; ++i) { 692 | id = item->compositeKey.keyValues[i]; 693 | if (id == comment_value_not_specified) { 694 | continue; 695 | } 696 | string_struct = hash_search(id_to_string, (void *) &id, HASH_FIND, &found); 697 | string_struct->counter -= (int)(counters->usage + 0.5); 698 | if (string_struct->counter == 0) { 699 | hash_search(id_to_string, (void *) &id, HASH_REMOVE, &found); 700 | hash_search(string_to_id, (void *) string_struct->string, HASH_REMOVE, &found); 701 | global_variables->currents_strings_count -= 1; 702 | } 703 | } 704 | LWLockRelease(&global_variables->lock); 705 | 706 | } 707 | 708 | static void 709 | pgcs_init() { 710 | LWLockAcquire(&global_variables->lock, LW_EXCLUSIVE); 711 | memset(&global_variables->commentKeys, '\0', sizeof(global_variables->commentKeys)); 712 | global_variables->max_strings_count_achieved = false; 713 | global_variables->keys_count = 0; 714 | global_variables->strings_overflow_by = 0; 715 | global_variables->out_of_shared_memory = false; 716 | global_variables->keys_overflow = false; 717 | SpinLockInit(&global_variables->overflow_mutex); 718 | LWLockRelease(&global_variables->lock); 719 | } 720 | 721 | static void 722 | pgcs_update_info() { 723 | if (global_variables == NULL) { 724 | return; 725 | } 726 | SpinLockAcquire(&global_variables->overflow_mutex); 727 | if (global_variables->max_strings_count_achieved) { 728 | elog(WARNING, "pg_comment_stats: Too many unique strings. Overflow by %d (%f%%)", 729 | global_variables->strings_overflow_by, (double)global_variables->strings_overflow_by / global_variables->items_count); 730 | } 731 | if (global_variables->keys_overflow) { 732 | elog(WARNING, "pg_comment_stats: Can not store more than %d parameters", max_parameters_count); 733 | } 734 | global_variables->keys_overflow = false; 735 | global_variables->strings_overflow_by = 0; 736 | global_variables->max_strings_count_achieved = false; 737 | SpinLockRelease(&global_variables->overflow_mutex); 738 | } 739 | 740 | void 741 | pg_comment_stats_main(Datum main_arg) { 742 | TimestampTz timestamp; 743 | int64 wait_microsec; 744 | /* Register functions for SIGTERM management */ 745 | pqsignal(SIGTERM, pg_comment_stats_sigterm); 746 | 747 | /* We're now ready to receive signals */ 748 | BackgroundWorkerUnblockSignals(); 749 | LWLockInitialize(&global_variables->lock, LWLockNewTrancheId()); 750 | 751 | pgcs_init(); 752 | wait_microsec = global_variables->bucket_duration * 1e6; 753 | while (!got_sigterm) { 754 | int rc; 755 | 756 | /* Wait necessary amount of time */ 757 | rc = WaitLatch(&MyProc->procLatch, 758 | WL_LATCH_SET | WL_TIMEOUT | WL_POSTMASTER_DEATH, wait_microsec / 1000, PG_WAIT_EXTENSION); 759 | /* Emergency bailout if postmaster has died */ 760 | if (rc & WL_POSTMASTER_DEATH) 761 | proc_exit(1); 762 | /* Process signals */ 763 | timestamp = GetCurrentTimestamp(); 764 | ResetLatch(&MyProc->procLatch); 765 | if (got_sigterm) { 766 | /* Simply exit */ 767 | elog(DEBUG1, "bgworker pg_comment_stats signal: processed SIGTERM"); 768 | proc_exit(0); 769 | } 770 | /* Main work happens here */ 771 | pgcs_update_info(); 772 | if (global_variables->out_of_shared_memory) { 773 | elog(WARNING, "pg_comment_stats: out of shared memory"); 774 | global_variables->out_of_shared_memory = false; 775 | } 776 | pgtb_tick(extension_name); 777 | wait_microsec = (int64) global_variables->bucket_duration * 1e6 - (GetCurrentTimestamp() - timestamp); 778 | if (wait_microsec < 0) 779 | wait_microsec = 0; 780 | } 781 | 782 | /* No problems, so clean exit */ 783 | proc_exit(0); 784 | } 785 | 786 | static char *escape_json_string(const char *str) { 787 | size_t str_len = strlen(str); 788 | size_t pstr_len = 2 * (str_len + 1); 789 | int i, j = 0; 790 | char *pstr = palloc(pstr_len); 791 | for (i = 0; i < str_len; i++) { 792 | char ch = str[i]; 793 | switch (ch) { 794 | case '\\': 795 | pstr[j++] = '\\'; 796 | pstr[j++] = '\\'; 797 | break; 798 | case '"': 799 | pstr[j++] = '\\'; 800 | pstr[j++] = '"'; 801 | break; 802 | case '\n': 803 | pstr[j++] = '\\'; 804 | pstr[j++] = 'n'; 805 | break; 806 | case '\r': 807 | pstr[j++] = '\\'; 808 | pstr[j++] = 'r'; 809 | break; 810 | case '\t': 811 | pstr[j++] = '\\'; 812 | pstr[j++] = 't'; 813 | break; 814 | case '\b': 815 | pstr[j++] = '\\'; 816 | pstr[j++] = 'b'; 817 | break; 818 | case '\f': 819 | pstr[j++] = '\\'; 820 | pstr[j++] = 'f'; 821 | break; 822 | default: 823 | pstr[j++] = ch; 824 | } 825 | if (j == pstr_len) break; 826 | } 827 | pstr[j++] = '\0'; 828 | return pstr; 829 | } 830 | 831 | 832 | PG_FUNCTION_INFO_V1(pgcs_get_stats); 833 | PG_FUNCTION_INFO_V1(pgcs_get_stats_time_interval); 834 | 835 | Datum 836 | get_jsonb_datum_from_key(pgcsCompositeKey *compositeKey) { 837 | StringInfoData strbuf; 838 | int i; 839 | char component[max_parameter_length]; 840 | char *escaped_component; 841 | const char* const_component; 842 | bool some_value_stored; 843 | int keys_count; 844 | char commentKeys[max_parameters_count][max_parameter_length]; 845 | 846 | some_value_stored = false; 847 | initStringInfo(&strbuf); 848 | appendStringInfoChar(&strbuf, '{'); 849 | 850 | memcpy(&commentKeys, &global_variables->commentKeys, sizeof(commentKeys)); 851 | keys_count = global_variables->keys_count; 852 | 853 | for (i = 0; i < keys_count; ++i) { 854 | if (compositeKey->keyValues[i] == comment_value_not_specified) { 855 | continue; 856 | } 857 | memset(&component, '\0', sizeof(component)); 858 | if (i > 0 && some_value_stored) 859 | appendStringInfoChar(&strbuf, ','); 860 | get_string_from_id(compositeKey->keyValues[i], (char*)&component); 861 | const_component = component; 862 | escaped_component = escape_json_string(const_component); 863 | 864 | appendStringInfo(&strbuf, "\"%s\":\"%s\"", (char*)&commentKeys[i], escaped_component); 865 | some_value_stored = true; 866 | pfree(escaped_component); 867 | } 868 | appendStringInfoChar(&strbuf, '}'); 869 | appendStringInfoChar(&strbuf, '\0'); 870 | 871 | return DirectFunctionCall1(jsonb_in, CStringGetDatum(strbuf.data)); 872 | } 873 | 874 | Datum 875 | pgcs_get_stats(PG_FUNCTION_ARGS) { 876 | 877 | TimestampTz timestamp_left; 878 | TimestampTz timestamp_right; 879 | 880 | timestamp_right = GetCurrentTimestamp(); 881 | timestamp_left = TimestampTzPlusMilliseconds(GetCurrentTimestamp(), (int64) global_variables->bucket_duration * (-buckets_count) * 1e3); 882 | 883 | return pgcs_internal_get_stats_time_interval(timestamp_left, timestamp_right, fcinfo); 884 | } 885 | 886 | Datum 887 | pgcs_get_stats_time_interval(PG_FUNCTION_ARGS) { 888 | TimestampTz timestamp_left; 889 | TimestampTz timestamp_right; 890 | 891 | timestamp_left = PG_GETARG_TIMESTAMP(0); 892 | timestamp_right = PG_GETARG_TIMESTAMP(1); 893 | 894 | return pgcs_internal_get_stats_time_interval(timestamp_left, timestamp_right, fcinfo); 895 | } 896 | 897 | /* TODO: Currently it's not possible to remove key from stats. Maybe add this possibility? */ 898 | static Datum 899 | pgcs_internal_get_stats_time_interval(TimestampTz timestamp_left, TimestampTz timestamp_right, FunctionCallInfo fcinfo) { 900 | ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; 901 | Tuplestorestate *tupstore; 902 | TupleDesc tupdesc; 903 | MemoryContext per_query_ctx; 904 | MemoryContext oldcontext; 905 | int message_id; 906 | pgcsBucketItem key; 907 | pgskCounters value; 908 | uint64 reads, writes; 909 | char timestamp_left_s[max_parameter_length]; 910 | char timestamp_right_s[max_parameter_length]; 911 | 912 | int items_count; 913 | void* result_ptr; 914 | int length; 915 | int i; 916 | 917 | Datum values[PG_STAT_KCACHE_COLS + 1]; 918 | bool nulls[PG_STAT_KCACHE_COLS + 1]; 919 | 920 | /* Shmem structs not ready yet */ 921 | if (!global_variables) 922 | ereport(ERROR, 923 | (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), 924 | errmsg("pg_comment_stats must be loaded via shared_preload_libraries"))); 925 | /* check to see if caller supports us returning a tuplestore */ 926 | if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo)) 927 | ereport(ERROR, 928 | (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), 929 | errmsg("set-valued function called in context that cannot accept a set"))); 930 | if (!(rsinfo->allowedModes & SFRM_Materialize)) 931 | ereport(ERROR, 932 | (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), 933 | errmsg("materialize mode required, but it is not allowed in this context"))); 934 | 935 | /* Build a tuple descriptor for our result type */ 936 | if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) 937 | ereport(ERROR, 938 | (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), 939 | errmsg("return type must be a row type"))); 940 | per_query_ctx = rsinfo->econtext->ecxt_per_query_memory; 941 | oldcontext = MemoryContextSwitchTo(per_query_ctx); 942 | 943 | tupstore = tuplestore_begin_heap(true, false, work_mem); 944 | rsinfo->returnMode = SFRM_Materialize; 945 | rsinfo->setResult = tupstore; 946 | rsinfo->setDesc = tupdesc; 947 | MemoryContextSwitchTo(oldcontext); 948 | 949 | MemSet(values, 0, sizeof(values)); 950 | MemSet(nulls, 0, sizeof(nulls)); 951 | MemSet(&key, 0, sizeof(pgcsBucketItem)); 952 | LWLockAcquire(&global_variables->lock, LW_EXCLUSIVE); 953 | 954 | items_count = global_variables->items_count; 955 | result_ptr = (void*) palloc(items_count * (sizeof(pgcsBucketItem) + sizeof(pgskCounters))); 956 | memset(result_ptr, 0, items_count * (sizeof(pgcsBucketItem) + sizeof(pgskCounters))); 957 | pgtb_get_stats_time_interval(extension_name, ×tamp_left, ×tamp_right, result_ptr, &length); 958 | 959 | strcpy(timestamp_left_s, timestamptz_to_str(timestamp_left)); 960 | strcpy(timestamp_right_s, timestamptz_to_str(timestamp_right)); 961 | 962 | elog(NOTICE, "pgcs: Show stats from '%s' to '%s'", timestamp_left_s, timestamp_right_s); 963 | 964 | /* put to tuplestore and clear bucket index -1 */ 965 | for (message_id = 0; message_id < length; ++message_id) { 966 | memcpy(&key, 967 | (char*)result_ptr + (sizeof(pgcsBucketItem) + sizeof(pgskCounters)) * message_id, 968 | sizeof(pgcsBucketItem)); 969 | memcpy(&value, 970 | (char*)result_ptr + (sizeof(pgcsBucketItem) + sizeof(pgskCounters)) * message_id + sizeof(pgcsBucketItem), 971 | sizeof(pgskCounters)); 972 | 973 | MemSet(nulls, false, sizeof(nulls)); 974 | i = 0; 975 | values[i++] = get_jsonb_datum_from_key(&key.compositeKey); 976 | 977 | values[i++] = Int64GetDatum(value.usage); 978 | values[i++] = ObjectIdGetDatum(key.user); 979 | values[i++] = ObjectIdGetDatum(key.database); 980 | 981 | #ifdef HAVE_GETRUSAGE 982 | reads = value.reads * RUSAGE_BLOCK_SIZE; 983 | writes = value.writes * RUSAGE_BLOCK_SIZE; 984 | values[i++] = UInt64GetDatum(reads); 985 | values[i++] = UInt64GetDatum(writes); 986 | #else 987 | nulls[i++] = true; /* reads */ 988 | nulls[i++] = true; /* writes */ 989 | #endif 990 | values[i++] = Float8GetDatumFast(value.utime); 991 | values[i++] = Float8GetDatumFast(value.stime); 992 | 993 | #ifdef HAVE_GETRUSAGE 994 | values[i++] = Int64GetDatumFast(value.minflts); 995 | values[i++] = Int64GetDatumFast(value.majflts); 996 | values[i++] = Int64GetDatumFast(value.nswaps); 997 | values[i++] = Int64GetDatumFast(value.msgsnds); 998 | values[i++] = Int64GetDatumFast(value.msgrcvs); 999 | values[i++] = Int64GetDatumFast(value.nsignals); 1000 | values[i++] = Int64GetDatumFast(value.nvcsws); 1001 | values[i++] = Int64GetDatumFast(value.nivcsws); 1002 | 1003 | #else 1004 | nulls[i++] = true; /* minflts */ 1005 | nulls[i++] = true; /* majflts */ 1006 | nulls[i++] = true; /* nswaps */ 1007 | nulls[i++] = true; /* msgsnds */ 1008 | nulls[i++] = true; /* msgrcvs */ 1009 | nulls[i++] = true; /* nsignals */ 1010 | nulls[i++] = true; /* nvcsws */ 1011 | nulls[i++] = true; /* nivcsws */ 1012 | #endif 1013 | tuplestore_putvalues(tupstore, tupdesc, values, nulls); 1014 | } 1015 | pfree(result_ptr); 1016 | LWLockRelease(&global_variables->lock); 1017 | return (Datum) 0; 1018 | } 1019 | 1020 | PG_FUNCTION_INFO_V1(pgcs_exclude_key); 1021 | 1022 | Datum 1023 | pgcs_exclude_key(PG_FUNCTION_ARGS) { 1024 | char key_copy[max_parameter_length]; 1025 | if (!global_variables) 1026 | ereport(ERROR, 1027 | (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), 1028 | errmsg("pg_comment_stats must be loaded via shared_preload_libraries"))); 1029 | LWLockAcquire(&global_variables->lock, LW_EXCLUSIVE); 1030 | if (global_variables->excluded_keys_count == max_parameters_count) { 1031 | elog(WARNING, "pgcs: Can't exclude more than %d keys. You can drop existing keys by 'pgcs_reset_excluded_keys'", 1032 | max_parameters_count); 1033 | } 1034 | else { 1035 | memset(&key_copy, 0, sizeof(key_copy)); 1036 | strlcpy((char*)&key_copy, PG_GETARG_CSTRING(0), sizeof(key_copy) - 1); 1037 | if (is_key_excluded((char*)&key_copy)) { 1038 | elog(WARNING, "pgcs: Key already excluded."); 1039 | } else { 1040 | strlcpy(&global_variables->excluded_keys[global_variables->excluded_keys_count][0], PG_GETARG_CSTRING(0), max_parameter_length - 1); 1041 | global_variables->excluded_keys_count++; 1042 | } 1043 | } 1044 | LWLockRelease(&global_variables->lock); 1045 | 1046 | PG_RETURN_VOID(); 1047 | } 1048 | 1049 | PG_FUNCTION_INFO_V1(pgcs_get_excluded_keys); 1050 | 1051 | Datum 1052 | pgcs_get_excluded_keys(PG_FUNCTION_ARGS) { 1053 | ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; 1054 | Tuplestorestate *tupstore; 1055 | TupleDesc tupdesc; 1056 | MemoryContext per_query_ctx; 1057 | MemoryContext oldcontext; 1058 | Datum values[1]; 1059 | bool nulls[1]; 1060 | int i; 1061 | 1062 | /* Shmem structs not ready yet */ 1063 | if (!global_variables) 1064 | ereport(ERROR, 1065 | (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), 1066 | errmsg("pg_comment_stats must be loaded via shared_preload_libraries"))); 1067 | /* check to see if caller supports us returning a tuplestore */ 1068 | if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo)) 1069 | ereport(ERROR, 1070 | (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), 1071 | errmsg("set-valued function called in context that cannot accept a set"))); 1072 | if (!(rsinfo->allowedModes & SFRM_Materialize)) 1073 | ereport(ERROR, 1074 | (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), 1075 | errmsg("materialize mode required, but it is not allowed in this context"))); 1076 | 1077 | /* Build a tuple descriptor for our result type */ 1078 | if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_SCALAR) 1079 | ereport(ERROR, 1080 | (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), 1081 | errmsg("return type must be a scalar type"))); 1082 | per_query_ctx = rsinfo->econtext->ecxt_per_query_memory; 1083 | oldcontext = MemoryContextSwitchTo(per_query_ctx); 1084 | 1085 | tupstore = tuplestore_begin_heap(true, false, work_mem); 1086 | rsinfo->returnMode = SFRM_Materialize; 1087 | rsinfo->setResult = tupstore; 1088 | 1089 | rsinfo->setDesc = tupdesc; 1090 | MemoryContextSwitchTo(oldcontext); 1091 | 1092 | MemSet(values, 0, sizeof(values)); 1093 | MemSet(nulls, 0, sizeof(nulls)); 1094 | #if PG_VERSION_NUM >= 120000 1095 | tupdesc = CreateTemplateTupleDesc(1); 1096 | #else 1097 | tupdesc = CreateTemplateTupleDesc(1, false); 1098 | #endif 1099 | TupleDescInitEntry(tupdesc, (AttrNumber) 1, "excluded_key", 1100 | TEXTOID, -1, 0); 1101 | tupdesc = BlessTupleDesc(tupdesc); 1102 | 1103 | LWLockAcquire(&global_variables->lock, LW_EXCLUSIVE); 1104 | for (i = 0; i < global_variables->excluded_keys_count; ++i) { 1105 | values[0] = CStringGetTextDatum(global_variables->excluded_keys[i]); 1106 | nulls[0] = false; 1107 | tuplestore_putvalues(tupstore, tupdesc, values, nulls); 1108 | } 1109 | 1110 | LWLockRelease(&global_variables->lock); 1111 | return (Datum) 0; 1112 | } 1113 | 1114 | PG_FUNCTION_INFO_V1(pgcs_get_buffer_stats); 1115 | 1116 | Datum 1117 | pgcs_get_buffer_stats(PG_FUNCTION_ARGS) { 1118 | ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; 1119 | Tuplestorestate *tupstore; 1120 | TupleDesc tupdesc; 1121 | MemoryContext per_query_ctx; 1122 | MemoryContext oldcontext; 1123 | Datum values[BUFFER_STATS_COUNT]; 1124 | bool nulls[BUFFER_STATS_COUNT]; 1125 | 1126 | /* Shmem structs not ready yet */ 1127 | if (!global_variables) 1128 | ereport(ERROR, 1129 | (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), 1130 | errmsg("pg_comment_stats must be loaded via shared_preload_libraries"))); 1131 | /* check to see if caller supports us returning a tuplestore */ 1132 | if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo)) 1133 | ereport(ERROR, 1134 | (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), 1135 | errmsg("set-valued function called in context that cannot accept a set"))); 1136 | if (!(rsinfo->allowedModes & SFRM_Materialize)) 1137 | ereport(ERROR, 1138 | (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), 1139 | errmsg("materialize mode required, but it is not allowed in this context"))); 1140 | 1141 | /* Build a tuple descriptor for our result type */ 1142 | if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) 1143 | ereport(ERROR, 1144 | (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), 1145 | errmsg("return type must be a row type"))); 1146 | per_query_ctx = rsinfo->econtext->ecxt_per_query_memory; 1147 | oldcontext = MemoryContextSwitchTo(per_query_ctx); 1148 | 1149 | tupstore = tuplestore_begin_heap(true, false, work_mem); 1150 | rsinfo->returnMode = SFRM_Materialize; 1151 | rsinfo->setResult = tupstore; 1152 | 1153 | rsinfo->setDesc = tupdesc; 1154 | MemoryContextSwitchTo(oldcontext); 1155 | 1156 | MemSet(values, 0, sizeof(values)); 1157 | MemSet(nulls, 0, sizeof(nulls)); 1158 | #if PG_VERSION_NUM >= 120000 1159 | tupdesc = CreateTemplateTupleDesc(BUFFER_STATS_COUNT); 1160 | #else 1161 | tupdesc = CreateTemplateTupleDesc(BUFFER_STATS_COUNT, false); 1162 | #endif 1163 | TupleDescInitEntry(tupdesc, (AttrNumber) 1, "saved_strings_count", 1164 | INT4OID, -1, 0); 1165 | TupleDescInitEntry(tupdesc, (AttrNumber) 2, "available_strings_count", 1166 | INT4OID, -1, 0); 1167 | tupdesc = BlessTupleDesc(tupdesc); 1168 | 1169 | LWLockAcquire(&global_variables->lock, LW_EXCLUSIVE); 1170 | values[0] = Int32GetDatum(global_variables->currents_strings_count); 1171 | values[1] = Int32GetDatum(global_variables->items_count); 1172 | 1173 | LWLockRelease(&global_variables->lock); 1174 | tuplestore_putvalues(tupstore, tupdesc, values, nulls); 1175 | return (Datum) 0; 1176 | } 1177 | 1178 | PG_FUNCTION_INFO_V1(pgcs_reset_excluded_keys); 1179 | 1180 | Datum 1181 | pgcs_reset_excluded_keys(PG_FUNCTION_ARGS) { 1182 | if (!global_variables) 1183 | ereport(ERROR, 1184 | (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), 1185 | errmsg("pg_comment_stats must be loaded via shared_preload_libraries"))); 1186 | 1187 | LWLockAcquire(&global_variables->lock, LW_EXCLUSIVE); 1188 | memset(&global_variables->excluded_keys, '\0', sizeof(global_variables->excluded_keys)); 1189 | global_variables->excluded_keys_count = 0; 1190 | LWLockRelease(&global_variables->lock); 1191 | PG_RETURN_VOID(); 1192 | } 1193 | -------------------------------------------------------------------------------- /pg_comment_stats.control: -------------------------------------------------------------------------------- 1 | # pg_comment_stats extension 2 | comment = 'Kernel statistics gathering by comment keys.' 3 | default_version = '1.0' 4 | requires = 'pg_stat_statements,pg_stat_kcache' 5 | module_pathname = '$libdir/pg_comment_stats' 6 | relocatable = true 7 | -------------------------------------------------------------------------------- /pg_comment_stats.h: -------------------------------------------------------------------------------- 1 | #ifndef PGSK_AGGREGATED_STATS 2 | #define PGSK_AGGREGATED_STATS 3 | 4 | #include "postgres.h" 5 | 6 | #ifdef HAVE_SYS_RESOURCE_H 7 | #include 8 | #endif 9 | 10 | #ifndef HAVE_GETRUSAGE 11 | #define HAVE_GETRUSAGE 12 | #endif 13 | 14 | #include "funcapi.h" 15 | #include "miscadmin.h" 16 | #include "pgstat.h" 17 | #include "utils/jsonb.h" 18 | #include "executor/execdesc.h" 19 | #include "nodes/execnodes.h" 20 | #include "storage/ipc.h" 21 | #include "utils/builtins.h" 22 | #include "catalog/pg_type.h" 23 | 24 | #include "pg_stat_kcache/pg_stat_kcache.h" 25 | #include "pg_time_buffer/pg_time_buffer.h" 26 | #include "pg_comment_stats_constants.h" 27 | 28 | typedef struct pgcsStringFromId { 29 | int id; 30 | char string[max_parameter_length]; 31 | int counter; 32 | slock_t mutex; /* protects the counter only */ 33 | } pgcsStringFromId; 34 | 35 | typedef struct pgcsIdFromString { 36 | char string[max_parameter_length]; 37 | int id; 38 | } pgcsIdFromString; 39 | 40 | typedef struct pgcsCompositeKey { 41 | int keyValues[max_parameters_count]; 42 | } pgcsCompositeKey; 43 | 44 | typedef struct pgcsBucketItem { 45 | pgcsCompositeKey compositeKey; 46 | Oid database; 47 | Oid user; 48 | } pgcsBucketItem; 49 | 50 | Datum get_jsonb_datum_from_key(pgcsCompositeKey *); 51 | 52 | typedef struct global_info { 53 | char commentKeys[max_parameters_count][max_parameter_length]; 54 | char excluded_keys[max_parameters_count][max_parameter_length]; 55 | int excluded_keys_count; 56 | int keys_count; 57 | int currents_strings_count; 58 | int items_count; 59 | int bucket_duration; 60 | int strings_overflow_by; 61 | slock_t overflow_mutex; /* protects strings_overflow_by counter */ 62 | bool keys_overflow; 63 | bool out_of_shared_memory; 64 | LWLock lock; 65 | bool max_strings_count_achieved; 66 | 67 | pgcsBucketItem buckets[FLEXIBLE_ARRAY_MEMBER]; 68 | } GlobalInfo; 69 | 70 | void pgcs_register_bgworker(void); 71 | extern void pgtb_init(const char*, 72 | void (*add)(void*, void*), 73 | void (*on_delete)(void*, void*), 74 | int, 75 | uint64_t, 76 | uint64_t, 77 | uint64_t 78 | ); 79 | extern bool pgtb_put(const char*, void*, void*); 80 | extern void pgtb_tick(const char*); 81 | extern void pgtb_get_stats(const char*, void*, int*, TimestampTz*, TimestampTz*); 82 | extern void pgtb_get_stats_time_interval(const char*, TimestampTz*, TimestampTz*, void*, int*); 83 | extern int pgtb_get_items_count(uint64_t, uint64_t, uint64_t); 84 | 85 | bool pgcs_enabled; 86 | #endif 87 | -------------------------------------------------------------------------------- /pg_comment_stats_constants.h: -------------------------------------------------------------------------------- 1 | #define max_buffer_size_mb (1 << 11) - 1 2 | #define min_buffer_size_mb 1 3 | #define max_time_interval 360000 4 | #define min_time_interval 100 5 | #define buckets_count 100 6 | #define max_parameter_length 64 7 | #define max_parameters_count 5 8 | #define comment_key_not_specified -1 9 | #define comment_value_not_specified 0 10 | 11 | #define BUFFER_STATS_COUNT 2 12 | --------------------------------------------------------------------------------