You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by zw...@apache.org on 2020/06/09 21:43:02 UTC

[trafficserver] 01/02: code and documentation for lua states configuration and stats printing (#6571)

This is an automated email from the ASF dual-hosted git repository.

zwoop pushed a commit to branch 8.1.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git

commit 93753ed5688461c2a555adbcaf47a11c161c971d
Author: Brian Olsen <br...@comcast.com>
AuthorDate: Mon Apr 20 14:59:44 2020 -0600

    code and documentation for lua states configuration and stats printing (#6571)
    
    (cherry picked from commit 8369634bdff95d6e6a0fcdd04cd81d183e8e675a)
---
 doc/admin-guide/plugins/lua.en.rst                 |  54 ++++
 plugins/lua/ts_lua.c                               | 348 +++++++++++++++++++--
 plugins/lua/ts_lua_coroutine.h                     |  16 +-
 plugins/lua/ts_lua_util.c                          |  96 +++++-
 .../pluginTest/lua/gold/lifecycle.stderr.gold      |   6 +
 .../pluginTest/lua/gold/metrics.stdout.gold        |   3 +
 tests/gold_tests/pluginTest/lua/hello.lua          |  35 +++
 .../pluginTest/lua/lua_states_stats.test.py        |  99 ++++++
 8 files changed, 621 insertions(+), 36 deletions(-)

diff --git a/doc/admin-guide/plugins/lua.en.rst b/doc/admin-guide/plugins/lua.en.rst
index ef556f5..8cf69b1 100644
--- a/doc/admin-guide/plugins/lua.en.rst
+++ b/doc/admin-guide/plugins/lua.en.rst
@@ -127,6 +127,60 @@ If it is used as remap plugin, we can write the following in remap.config to def
 
     map http://a.tbcdn.cn/ http://inner.tbcdn.cn/ @plugin=/XXX/tslua.so @pparam=--states=64 @pparam=/XXX/test_hdr.lua
 
+The maximum number of allowed states is set to 256 which is also the
+default states value.  The default value can be globally changed by
+adding a configuration option to records.config.
+
+::
+
+    CONFIG proxy.config.plugin.lua.max_states INT 64
+
+Any per plugin --states value overrides this default value but must be less than or equal to this value.  This setting is not reloadable since it must be applied when all the lua states are first initialized.
+
+Profiling
+=========
+
+The lua module collects runtime statistics about the lua states, for remap
+and global instances.  Per state stats are constantly maintained and are
+made available through a lifecycle hook.  These may be accessed through:
+
+::
+
+    traffic_ctl plugin msg ts_lua stats_print
+
+Sample output:
+
+::
+
+    [Feb  5 19:00:15.072] ts_lua (remap) id:    0 gc_kb:   2508 gc_kb_max:   3491 threads:  417 threads_max:  438
+    [Feb  5 19:00:15.072] ts_lua (remap) id:    1 gc_kb:   1896 gc_kb_max:   3646 threads:  417 threads_max:  446
+    [Feb  5 19:00:15.072] ts_lua (remap) id:    2 gc_kb:   3376 gc_kb_max:   3740 threads:  417 threads_max:  442
+
+Max values may be reset at any time by running:
+
+::
+
+    traffic_ctl plugin msg ts_lua stats_reset
+
+
+Summary statistics are aggregated every 5s and are available as metrics.
+
+::
+
+    traffic_ctl metric match lua
+
+Sample output:
+
+::
+
+    plugin.lua.global.states 8
+    plugin.lua.remap.gc_bytes_min 4804608
+    plugin.lua.remap.gc_bytes_mean 5552537
+    plugin.lua.remap.gc_bytes_max 5779456
+    plugin.lua.remap.threads_min 31
+    plugin.lua.remap.threads_mean 44
+    plugin.lua.remap.threads_max 146
+
 TS API for Lua
 ==============
 
diff --git a/plugins/lua/ts_lua.c b/plugins/lua/ts_lua.c
index c88096c..dc7dd0e 100644
--- a/plugins/lua/ts_lua.c
+++ b/plugins/lua/ts_lua.c
@@ -25,36 +25,311 @@
 
 #define TS_LUA_MAX_STATE_COUNT 256
 
+#define TS_LUA_STATS_TIMEOUT 5000   // 5s -- convert to configurable
+#define TS_LUA_STATS_BUFFER_SIZE 10 // stats buffer
+
+#define TS_LUA_IND_STATE 0
+#define TS_LUA_IND_GC_BYTES 1
+#define TS_LUA_IND_THREADS 2
+#define TS_LUA_IND_SIZE 3
+
 static uint64_t ts_lua_http_next_id   = 0;
 static uint64_t ts_lua_g_http_next_id = 0;
 
-static ts_lua_main_ctx *ts_lua_main_ctx_array;
-static ts_lua_main_ctx *ts_lua_g_main_ctx_array;
+static ts_lua_main_ctx *ts_lua_main_ctx_array   = NULL;
+static ts_lua_main_ctx *ts_lua_g_main_ctx_array = NULL;
+
+// records.config entry injected by plugin
+static char const *const ts_lua_mgmt_state_str   = "proxy.config.plugin.lua.max_states";
+static char const *const ts_lua_mgmt_state_regex = "^[1-9][0-9]*$";
+
+// this is set the first time global configuration is probed.
+static int ts_lua_max_state_count = 0;
+
+// lifecycle message tag
+static char const *const print_tag = "stats_print";
+static char const *const reset_tag = "stats_reset";
+
+// stat record strings
+static char const *const ts_lua_stat_strs[] = {
+  "plugin.lua.remap.states",
+  "plugin.lua.remap.gc_bytes",
+  "plugin.lua.remap.threads",
+  NULL,
+};
+static char const *const ts_lua_g_stat_strs[] = {
+  "plugin.lua.global.states",
+  "plugin.lua.global.gc_bytes",
+  "plugin.lua.global.threads",
+  NULL,
+};
+
+typedef struct {
+  ts_lua_main_ctx *main_ctx_array;
+
+  int gc_kb;   // last collected gc in kb
+  int threads; // last collected number active threads
+
+  int stat_inds[TS_LUA_IND_SIZE]; // stats indices
+
+} ts_lua_plugin_stats;
+
+ts_lua_plugin_stats *
+create_plugin_stats(ts_lua_main_ctx *const main_ctx_array)
+{
+  ts_lua_plugin_stats *const stats = TSmalloc(sizeof(ts_lua_plugin_stats));
+  memset(stats, 0, sizeof(ts_lua_plugin_stats));
+
+  stats->main_ctx_array = main_ctx_array;
+
+  // sample buffers
+  stats->gc_kb   = 0;
+  stats->threads = 0;
+
+  char const *const *stat_strs = NULL;
+  int max_state_count          = 0;
+  if (main_ctx_array == ts_lua_main_ctx_array) { // remap
+    stat_strs       = ts_lua_stat_strs;
+    max_state_count = ts_lua_max_state_count;
+  } else { // global
+    stat_strs       = ts_lua_g_stat_strs;
+    max_state_count = ts_lua_max_state_count;
+  }
+
+  for (int ind = 0; ind < TS_LUA_IND_SIZE; ++ind) {
+    stats->stat_inds[ind] = TSStatCreate(stat_strs[ind], TS_RECORDDATATYPE_INT, TS_STAT_NON_PERSISTENT, TS_STAT_SYNC_SUM);
+  }
+
+  // initialize the number of states stat
+  int const sid = stats->stat_inds[TS_LUA_IND_STATE];
+  if (TS_ERROR != sid) {
+    TSStatIntSet(sid, max_state_count);
+  }
+
+  return stats;
+}
+
+ts_lua_main_ctx *
+create_lua_vms()
+{
+  ts_lua_main_ctx *ctx_array = NULL;
+
+  // Inject the setting into records.config
+  static bool ts_mgt_int_inserted = false;
+  if (!ts_mgt_int_inserted) {
+    if (TS_SUCCESS == TSMgmtIntCreate(TS_RECORDTYPE_CONFIG, ts_lua_mgmt_state_str, TS_LUA_MAX_STATE_COUNT,
+                                      TS_RECORDUPDATE_RESTART_TS, TS_RECORDCHECK_INT, ts_lua_mgmt_state_regex,
+                                      TS_RECORDACCESS_READ_ONLY)) {
+      TSDebug(TS_LUA_DEBUG_TAG, "[%s] registered config string %s: with default [%d]", __FUNCTION__, ts_lua_mgmt_state_str,
+              TS_LUA_MAX_STATE_COUNT);
+    } else {
+      TSError("[%s][%s] failed to register %s", TS_LUA_DEBUG_TAG, __FUNCTION__, ts_lua_mgmt_state_str);
+    }
+    ts_mgt_int_inserted = true;
+  }
+
+  if (0 == ts_lua_max_state_count) {
+    TSMgmtInt mgmt_state = 0;
+
+    if (TS_SUCCESS != TSMgmtIntGet(ts_lua_mgmt_state_str, &mgmt_state)) {
+      TSDebug(TS_LUA_DEBUG_TAG, "[%s] setting max state to default: %d", __FUNCTION__, TS_LUA_MAX_STATE_COUNT);
+      ts_lua_max_state_count = TS_LUA_MAX_STATE_COUNT;
+    } else {
+      ts_lua_max_state_count = (int)mgmt_state;
+      TSDebug(TS_LUA_DEBUG_TAG, "[%s] found %s: [%d]", __FUNCTION__, ts_lua_mgmt_state_str, (int)ts_lua_max_state_count);
+    }
+
+    if (ts_lua_max_state_count < 1) {
+      TSError("[ts_lua][%s] invalid %s: %d", __FUNCTION__, ts_lua_mgmt_state_str, ts_lua_max_state_count);
+      ts_lua_max_state_count = 0;
+      return NULL;
+    }
+  }
+
+  ctx_array = TSmalloc(sizeof(ts_lua_main_ctx) * ts_lua_max_state_count);
+  memset(ctx_array, 0, sizeof(ts_lua_main_ctx) * ts_lua_max_state_count);
+
+  int const ret = ts_lua_create_vm(ctx_array, ts_lua_max_state_count);
+
+  if (ret) {
+    ts_lua_destroy_vm(ctx_array, ts_lua_max_state_count);
+    TSfree(ctx_array);
+    ctx_array = NULL;
+    return NULL;
+  }
+
+  // Initalize the GC numbers, no need to lock here
+  for (int index = 0; index < ts_lua_max_state_count; ++index) {
+    ts_lua_main_ctx *const main_ctx = (ctx_array + index);
+    lua_State *const lstate         = main_ctx->lua;
+    ts_lua_ctx_stats *const stats   = main_ctx->stats;
+
+    stats->gc_kb = stats->gc_kb_max = lua_getgccount(lstate);
+  }
+
+  return ctx_array;
+}
+
+// dump exhaustive per state summary stats
+static void
+collectStats(ts_lua_plugin_stats *const plugin_stats)
+{
+  TSMgmtInt gc_kb_total   = 0;
+  TSMgmtInt threads_total = 0;
+
+  ts_lua_main_ctx *const main_ctx_array = plugin_stats->main_ctx_array;
+
+  // aggregate stats on the states
+  for (int index = 0; index < ts_lua_max_state_count; ++index) {
+    ts_lua_main_ctx *const main_ctx = (main_ctx_array + index);
+    if (NULL != main_ctx) {
+      ts_lua_ctx_stats *const stats = main_ctx->stats;
+
+      TSMutexLock(stats->mutexp);
+      gc_kb_total += (TSMgmtInt)stats->gc_kb;
+      threads_total += (TSMgmtInt)stats->threads;
+      TSMutexUnlock(stats->mutexp);
+    }
+  }
+
+  // set the stats sample slot
+  plugin_stats->gc_kb   = gc_kb_total;
+  plugin_stats->threads = threads_total;
+}
+
+static void
+publishStats(ts_lua_plugin_stats *const plugin_stats)
+{
+  TSMgmtInt const gc_bytes = plugin_stats->gc_kb * 1024;
+  TSStatIntSet(plugin_stats->stat_inds[TS_LUA_IND_GC_BYTES], gc_bytes);
+  TSStatIntSet(plugin_stats->stat_inds[TS_LUA_IND_THREADS], plugin_stats->threads);
+}
+
+// dump exhaustive per state summary stats
+static int
+statsHandler(TSCont contp, TSEvent event, void *edata)
+{
+  ts_lua_plugin_stats *const plugin_stats = (ts_lua_plugin_stats *)TSContDataGet(contp);
+
+  collectStats(plugin_stats);
+  publishStats(plugin_stats);
+
+  TSContSchedule(contp, TS_LUA_STATS_TIMEOUT, TS_THREAD_POOL_TASK);
+
+  return TS_EVENT_NONE;
+}
+
+static void
+get_time_now_str(char *const buf, size_t const buflen)
+{
+  TSHRTime const timenowusec = TShrtime();
+  int64_t const timemsec     = (int64_t)(timenowusec / 1000000);
+  time_t const timesec       = (time_t)(timemsec / 1000);
+  int const ms               = (int)(timemsec % 1000);
+
+  struct tm tm;
+  gmtime_r(&timesec, &tm);
+  size_t const dtlen = strftime(buf, buflen, "%b %e %H:%M:%S", &tm);
+
+  // tack on the ms
+  snprintf(buf + dtlen, buflen - dtlen, ".%03d", ms);
+}
+
+// dump exhaustive per state summary stats
+static int
+lifecycleHandler(TSCont contp, TSEvent event, void *edata)
+{
+  // ensure the message is for ts_lua
+  TSPluginMsg *const msgp = (TSPluginMsg *)edata;
+  if (0 != strncasecmp(msgp->tag, TS_LUA_DEBUG_TAG, strlen(msgp->tag))) {
+    return TS_EVENT_NONE;
+  }
+
+  ts_lua_main_ctx *const main_ctx_array = (ts_lua_main_ctx *)TSContDataGet(contp);
+
+  static char const *const remapstr  = "remap";
+  static char const *const globalstr = "global";
+
+  char const *labelstr = NULL;
+
+  if (main_ctx_array == ts_lua_main_ctx_array) {
+    labelstr = remapstr;
+  } else {
+    labelstr = globalstr;
+  }
+
+  char timebuf[128];
+  get_time_now_str(timebuf, 128);
+
+  char const *const msgstr = (char *)msgp->data;
+  enum State { Print, Reset } state;
+  state                      = Print;
+  size_t const reset_tag_len = strlen(reset_tag);
+
+  if (reset_tag_len <= msgp->data_size && 0 == strncasecmp(reset_tag, msgstr, reset_tag_len)) {
+    TSDebug(TS_LUA_DEBUG_TAG, "[%s] LIFECYCLE_MSG: %s", __FUNCTION__, reset_tag);
+    state = Reset;
+    fprintf(stderr, "[%s] %s (%s) resetting per state gc_kb_max and threads_max\n", timebuf, TS_LUA_DEBUG_TAG, labelstr);
+  } else {
+    TSDebug(TS_LUA_DEBUG_TAG, "[%s] LIFECYCLE_MSG: %s", __FUNCTION__, print_tag);
+  }
+
+  for (int index = 0; index < ts_lua_max_state_count; ++index) {
+    ts_lua_main_ctx *const main_ctx = (main_ctx_array + index);
+    if (NULL != main_ctx) {
+      ts_lua_ctx_stats *const stats = main_ctx->stats;
+      if (NULL != main_ctx) {
+        TSMutexLock(stats->mutexp);
+
+        switch (state) {
+        case Reset:
+          stats->threads_max = stats->threads;
+          stats->gc_kb_max   = stats->gc_kb;
+          break;
+
+        case Print:
+        default:
+          fprintf(stderr, "[%s] %s (%s) id: %3d gc_kb: %6d gc_kb_max: %6d threads: %4d threads_max: %4d\n", timebuf,
+                  TS_LUA_DEBUG_TAG, labelstr, index, stats->gc_kb, stats->gc_kb_max, stats->threads, stats->threads_max);
+          break;
+        }
+
+        TSMutexUnlock(stats->mutexp);
+      }
+    }
+  }
+
+  return TS_EVENT_NONE;
+}
 
 TSReturnCode
 TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size)
 {
-  int ret;
-
   if (!api_info || api_info->size < sizeof(TSRemapInterface)) {
     strncpy(errbuf, "[TSRemapInit] - Incorrect size of TSRemapInterface structure", errbuf_size - 1);
     errbuf[errbuf_size - 1] = '\0';
     return TS_ERROR;
   }
 
-  if (ts_lua_main_ctx_array != NULL) {
-    return TS_SUCCESS;
-  }
-
-  ts_lua_main_ctx_array = TSmalloc(sizeof(ts_lua_main_ctx) * TS_LUA_MAX_STATE_COUNT);
-  memset(ts_lua_main_ctx_array, 0, sizeof(ts_lua_main_ctx) * TS_LUA_MAX_STATE_COUNT);
+  if (NULL == ts_lua_main_ctx_array) {
+    ts_lua_main_ctx_array = create_lua_vms();
+    if (NULL != ts_lua_main_ctx_array) {
+      TSCont const lcontp = TSContCreate(lifecycleHandler, TSMutexCreate());
+      TSContDataSet(lcontp, ts_lua_main_ctx_array);
+      TSLifecycleHookAdd(TS_LIFECYCLE_MSG_HOOK, lcontp);
 
-  ret = ts_lua_create_vm(ts_lua_main_ctx_array, TS_LUA_MAX_STATE_COUNT);
+      ts_lua_plugin_stats *const plugin_stats = create_plugin_stats(ts_lua_main_ctx_array);
 
-  if (ret) {
-    ts_lua_destroy_vm(ts_lua_main_ctx_array, TS_LUA_MAX_STATE_COUNT);
-    TSfree(ts_lua_main_ctx_array);
-    return TS_ERROR;
+      // start the stats management
+      if (NULL != plugin_stats) {
+        TSDebug(TS_LUA_DEBUG_TAG, "Starting up stats management continuation");
+        TSCont const scontp = TSContCreate(statsHandler, TSMutexCreate());
+        TSContDataSet(scontp, plugin_stats);
+        TSContSchedule(scontp, TS_LUA_STATS_TIMEOUT, TS_THREAD_POOL_TASK);
+      }
+    } else {
+      return TS_ERROR;
+    }
   }
 
   return TS_SUCCESS;
@@ -67,7 +342,7 @@ TSRemapNewInstance(int argc, char *argv[], void **ih, char *errbuf, int errbuf_s
   char script[TS_LUA_MAX_SCRIPT_FNAME_LENGTH];
   char *inline_script                  = "";
   int fn                               = 0;
-  int states                           = TS_LUA_MAX_STATE_COUNT;
+  int states                           = ts_lua_max_state_count;
   static const struct option longopt[] = {
     {"states", required_argument, 0, 's'},
     {"inline", required_argument, 0, 'i'},
@@ -84,7 +359,7 @@ TSRemapNewInstance(int argc, char *argv[], void **ih, char *errbuf, int errbuf_s
     switch (opt) {
     case 's':
       states = atoi(optarg);
-      TSDebug(TS_LUA_DEBUG_TAG, "[%s] setting number of lua VM [%d]", __FUNCTION__, states);
+      TSDebug(TS_LUA_DEBUG_TAG, "[%s] setting number of lua VMs [%d]", __FUNCTION__, states);
       // set state
       break;
     case 'i':
@@ -96,9 +371,9 @@ TSRemapNewInstance(int argc, char *argv[], void **ih, char *errbuf, int errbuf_s
     }
   }
 
-  if (states > TS_LUA_MAX_STATE_COUNT || states < 1) {
+  if (states < 1 || ts_lua_max_state_count < states) {
     snprintf(errbuf, errbuf_size, "[TSRemapNewInstance] - invalid state in option input. Must be between 1 and %d",
-             TS_LUA_MAX_STATE_COUNT);
+             ts_lua_max_state_count);
     return TS_ERROR;
   }
 
@@ -459,19 +734,27 @@ TSPluginInit(int argc, const char *argv[])
     TSError("[ts_lua] Plugin registration failed");
   }
 
-  int ret                 = 0;
-  ts_lua_g_main_ctx_array = TSmalloc(sizeof(ts_lua_main_ctx) * TS_LUA_MAX_STATE_COUNT);
-  memset(ts_lua_g_main_ctx_array, 0, sizeof(ts_lua_main_ctx) * TS_LUA_MAX_STATE_COUNT);
+  if (NULL == ts_lua_g_main_ctx_array) {
+    ts_lua_g_main_ctx_array = create_lua_vms();
+    if (NULL != ts_lua_g_main_ctx_array) {
+      TSCont const contp = TSContCreate(lifecycleHandler, TSMutexCreate());
+      TSContDataSet(contp, ts_lua_g_main_ctx_array);
+      TSLifecycleHookAdd(TS_LIFECYCLE_MSG_HOOK, contp);
 
-  ret = ts_lua_create_vm(ts_lua_g_main_ctx_array, TS_LUA_MAX_STATE_COUNT);
+      ts_lua_plugin_stats *const plugin_stats = create_plugin_stats(ts_lua_main_ctx_array);
 
-  if (ret) {
-    ts_lua_destroy_vm(ts_lua_g_main_ctx_array, TS_LUA_MAX_STATE_COUNT);
-    TSfree(ts_lua_g_main_ctx_array);
-    return;
+      if (NULL != plugin_stats) {
+        TSCont const scontp = TSContCreate(statsHandler, TSMutexCreate());
+        TSContDataSet(scontp, plugin_stats);
+        TSContSchedule(scontp, TS_LUA_STATS_TIMEOUT, TS_THREAD_POOL_TASK);
+      }
+    } else {
+      return;
+    }
   }
 
-  int states                           = TS_LUA_MAX_STATE_COUNT;
+  int states = ts_lua_max_state_count;
+
   int reload                           = 0;
   static const struct option longopt[] = {
     {"states", required_argument, 0, 's'},
@@ -499,8 +782,8 @@ TSPluginInit(int argc, const char *argv[])
     }
   }
 
-  if (states > TS_LUA_MAX_STATE_COUNT || states < 1) {
-    TSError("[ts_lua][%s] invalid # of states from option input. Must be between 1 and %d", __FUNCTION__, TS_LUA_MAX_STATE_COUNT);
+  if (states < 1 || ts_lua_max_state_count < states) {
+    TSError("[ts_lua][%s] invalid # of states from option input. Must be between 1 and %d", __FUNCTION__, ts_lua_max_state_count);
     return;
   }
 
@@ -532,8 +815,9 @@ TSPluginInit(int argc, const char *argv[])
   ts_lua_init_instance(conf);
 
   char errbuf[TS_LUA_MAX_STR_LENGTH];
-  int errbuf_len = sizeof(errbuf);
-  ret = ts_lua_add_module(conf, ts_lua_g_main_ctx_array, conf->states, argc - optind, (char **)&argv[optind], errbuf, errbuf_len);
+  int const errbuf_len = sizeof(errbuf);
+  int const ret =
+    ts_lua_add_module(conf, ts_lua_g_main_ctx_array, conf->states, argc - optind, (char **)&argv[optind], errbuf, errbuf_len);
 
   if (ret != 0) {
     TSError(errbuf, NULL);
diff --git a/plugins/lua/ts_lua_coroutine.h b/plugins/lua/ts_lua_coroutine.h
index 831066d..7f86812 100644
--- a/plugins/lua/ts_lua_coroutine.h
+++ b/plugins/lua/ts_lua_coroutine.h
@@ -28,11 +28,21 @@
 struct async_item;
 typedef int (*async_clean)(struct async_item *item);
 
+/* context stats */
+typedef struct {
+  TSMutex mutexp;  // mutex for the following stats
+  int gc_kb;       // last recorded gc kbytes
+  int gc_kb_max;   // maximum recorded gc kbytes
+  int threads;     // associated coroutines
+  int threads_max; // max coroutines
+} ts_lua_ctx_stats;
+
 /* main context*/
 typedef struct {
-  lua_State *lua; // basic lua vm, injected
-  TSMutex mutexp; // mutex for lua vm
-  int gref;       // reference for lua vm self, in reg table
+  lua_State *lua;          // basic lua vm, injected
+  TSMutex mutexp;          // mutex for lua vm
+  int gref;                // reference for lua vm self, in reg table
+  ts_lua_ctx_stats *stats; // per vm stats
 } ts_lua_main_ctx;
 
 /* coroutine */
diff --git a/plugins/lua/ts_lua_util.c b/plugins/lua/ts_lua_util.c
index 25eb1c2..76d86fd 100644
--- a/plugins/lua/ts_lua_util.c
+++ b/plugins/lua/ts_lua_util.c
@@ -40,6 +40,8 @@ static lua_State *ts_lua_new_state();
 static void ts_lua_init_registry(lua_State *L);
 static void ts_lua_init_globals(lua_State *L);
 static void ts_lua_inject_ts_api(lua_State *L);
+static ts_lua_ctx_stats *ts_lua_create_ctx_stats();
+static void ts_lua_destroy_ctx_stats(ts_lua_ctx_stats *stats);
 
 int
 ts_lua_create_vm(ts_lua_main_ctx *arr, int n)
@@ -58,6 +60,7 @@ ts_lua_create_vm(ts_lua_main_ctx *arr, int n)
     arr[i].gref   = luaL_ref(L, LUA_REGISTRYINDEX); /* L[REG][gref] = L[GLOBAL] */
     arr[i].lua    = L;
     arr[i].mutexp = TSMutexCreate();
+    arr[i].stats  = ts_lua_create_ctx_stats();
   }
 
   return 0;
@@ -68,11 +71,26 @@ ts_lua_destroy_vm(ts_lua_main_ctx *arr, int n)
 {
   int i;
   lua_State *L;
+  TSMutex mutexp;
+  ts_lua_ctx_stats *stats;
 
   for (i = 0; i < n; i++) {
     L = arr[i].lua;
-    if (L)
+    if (L) {
       lua_close(L);
+      arr[i].lua = NULL;
+    }
+    mutexp = arr[i].mutexp;
+    if (mutexp) {
+      TSMutexDestroy(mutexp);
+      arr[i].mutexp = NULL;
+    }
+
+    stats = arr[i].stats;
+    if (stats) {
+      ts_lua_destroy_ctx_stats(stats);
+      arr[i].stats = NULL;
+    }
   }
 
   return;
@@ -98,6 +116,29 @@ ts_lua_new_state()
   return L;
 }
 
+ts_lua_ctx_stats *
+ts_lua_create_ctx_stats()
+{
+  ts_lua_ctx_stats *stats = NULL;
+
+  stats = TSmalloc(sizeof(ts_lua_ctx_stats));
+  memset(stats, 0, sizeof(ts_lua_ctx_stats));
+
+  stats->mutexp = TSMutexCreate();
+
+  return stats;
+}
+
+void
+ts_lua_destroy_ctx_stats(ts_lua_ctx_stats *stats)
+{
+  if (stats) {
+    TSMutexDestroy(stats->mutexp);
+    stats->mutexp = NULL;
+    TSfree(stats);
+  }
+}
+
 ts_lua_instance_conf *
 ts_lua_script_registered(lua_State *L, char *script)
 {
@@ -495,6 +536,17 @@ ts_lua_create_async_ctx(lua_State *L, ts_lua_cont_info *hci, int n)
   crt->lua  = l;
   crt->ref  = luaL_ref(L, LUA_REGISTRYINDEX);
 
+  // update thread stats
+  ts_lua_main_ctx *const main_ctx = crt->mctx;
+  ts_lua_ctx_stats *const stats   = main_ctx->stats;
+
+  TSMutexLock(stats->mutexp);
+  ++stats->threads;
+  if (stats->threads_max < stats->threads) {
+    stats->threads_max = stats->threads;
+  }
+  TSMutexUnlock(stats->mutexp);
+
   // replace the param; start with 2 because first two params are not needed
   for (i = 2; i < n; i++) {
     lua_pushvalue(L, i + 1);
@@ -512,6 +564,14 @@ ts_lua_destroy_async_ctx(ts_lua_http_ctx *http_ctx)
 
   ci = &http_ctx->cinfo;
 
+  // update thread stats
+  ts_lua_main_ctx *const main_ctx = ci->routine.mctx;
+  ts_lua_ctx_stats *const stats   = main_ctx->stats;
+
+  TSMutexLock(stats->mutexp);
+  --stats->threads;
+  TSMutexUnlock(stats->mutexp);
+
   ts_lua_release_cont_info(ci);
   TSfree(http_ctx);
 }
@@ -574,6 +634,16 @@ ts_lua_create_http_ctx(ts_lua_main_ctx *main_ctx, ts_lua_instance_conf *conf)
   crt->lua  = l;
   crt->mctx = main_ctx;
 
+  // update thread stats
+  ts_lua_ctx_stats *const stats = main_ctx->stats;
+
+  TSMutexLock(stats->mutexp);
+  ++stats->threads;
+  if (stats->threads_max < stats->threads) {
+    stats->threads_max = stats->threads;
+  }
+  TSMutexUnlock(stats->mutexp);
+
   http_ctx->instance_conf = conf;
 
   ts_lua_set_http_ctx(l, http_ctx);
@@ -617,6 +687,14 @@ ts_lua_destroy_http_ctx(ts_lua_http_ctx *http_ctx)
     TSMBufferDestroy(http_ctx->cached_response_bufp);
   }
 
+  // update thread stats
+  ts_lua_main_ctx *const main_ctx = ci->routine.mctx;
+  ts_lua_ctx_stats *const stats   = main_ctx->stats;
+
+  TSMutexLock(stats->mutexp);
+  --stats->threads;
+  TSMutexUnlock(stats->mutexp);
+
   ts_lua_release_cont_info(ci);
   TSfree(http_ctx);
 }
@@ -800,6 +878,7 @@ ts_lua_http_cont_handler(TSCont contp, TSEvent ev, void *edata)
   rc = ret = 0;
 
   TSMutexLock(main_ctx->mutexp);
+
   ts_lua_set_cont_info(L, ci);
 
   switch (event) {
@@ -945,8 +1024,23 @@ ts_lua_http_cont_handler(TSCont contp, TSEvent ev, void *edata)
     break;
   }
 
+  // current memory in use by this state
+  int const gc_kb = lua_getgccount(L);
+
   TSMutexUnlock(main_ctx->mutexp);
 
+  // collect state memory stats
+  ts_lua_ctx_stats *const stats = main_ctx->stats;
+
+  TSMutexLock(stats->mutexp);
+  if (gc_kb != stats->gc_kb) {
+    stats->gc_kb = gc_kb;
+    if (stats->gc_kb_max < stats->gc_kb) {
+      stats->gc_kb_max = stats->gc_kb;
+    }
+  }
+  TSMutexUnlock(stats->mutexp);
+
   if (rc == 0) {
     TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
 
diff --git a/tests/gold_tests/pluginTest/lua/gold/lifecycle.stderr.gold b/tests/gold_tests/pluginTest/lua/gold/lifecycle.stderr.gold
new file mode 100644
index 0000000..1e4a4f8
--- /dev/null
+++ b/tests/gold_tests/pluginTest/lua/gold/lifecycle.stderr.gold
@@ -0,0 +1,6 @@
+``
+`` ts_lua (remap) id:   0 gc_kb: `` gc_kb_max: `` threads: `` threads_max: ``
+`` ts_lua (remap) id:   1 gc_kb: `` gc_kb_max: `` threads: `` threads_max: ``
+`` ts_lua (remap) id:   2 gc_kb: `` gc_kb_max: `` threads: `` threads_max: ``
+`` ts_lua (remap) id:   3 gc_kb: `` gc_kb_max: `` threads: `` threads_max: ``
+``
diff --git a/tests/gold_tests/pluginTest/lua/gold/metrics.stdout.gold b/tests/gold_tests/pluginTest/lua/gold/metrics.stdout.gold
new file mode 100644
index 0000000..4c1690b
--- /dev/null
+++ b/tests/gold_tests/pluginTest/lua/gold/metrics.stdout.gold
@@ -0,0 +1,3 @@
+plugin.lua.remap.states ``
+plugin.lua.remap.gc_bytes ``
+plugin.lua.remap.threads ``
diff --git a/tests/gold_tests/pluginTest/lua/hello.lua b/tests/gold_tests/pluginTest/lua/hello.lua
new file mode 100644
index 0000000..d5373f9
--- /dev/null
+++ b/tests/gold_tests/pluginTest/lua/hello.lua
@@ -0,0 +1,35 @@
+--  Licensed to the Apache Software Foundation (ASF) under one
+--  or more contributor license agreements.  See the NOTICE file
+--  distributed with this work for additional information
+--  regarding copyright ownership.  The ASF licenses this file
+--  to you under the Apache License, Version 2.0 (the
+--  "License"); you may not use this file except in compliance
+--  with the License.  You may obtain a copy of the License at
+--
+--  http://www.apache.org/licenses/LICENSE-2.0
+--
+--  Unless required by applicable law or agreed to in writing, software
+--  distributed under the License is distributed on an "AS IS" BASIS,
+--  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+--  See the License for the specific language governing permissions and
+--  limitations under the License.
+
+function do_remap()
+  if 'GET' == ts.client_request.get_method() then
+		if '/hello' == ts.client_request.get_uri() then
+			ts.http.set_resp(200, "Hello, World")
+		end
+  end
+end
+
+function origin_intercept_handler()
+	local body = 'Hello, World'
+  local resp =  'HTTP/1.0 200 OK\r\n' ..
+    'Server: Lua Black Magic\r\n' ..
+    'Content-Type: text/plain\r\n' ..
+		'Content-Length: ' .. string.len(body) .. '\r\n' ..
+		'\r\n'
+	ts.say(resp)
+	ts.say(body)
+	ts.flush()
+end
diff --git a/tests/gold_tests/pluginTest/lua/lua_states_stats.test.py b/tests/gold_tests/pluginTest/lua/lua_states_stats.test.py
new file mode 100644
index 0000000..97d07cc
--- /dev/null
+++ b/tests/gold_tests/pluginTest/lua/lua_states_stats.test.py
@@ -0,0 +1,99 @@
+'''
+'''
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+import os
+Test.Summary = '''
+Test lua states and stats functionality
+'''
+
+Test.SkipUnless(
+    Condition.PluginExists('tslua.so'),
+)
+
+Test.ContinueOnFail = True
+# Define default ATS
+server = Test.MakeOriginServer("server")
+
+ts = Test.MakeATSProcess("ts", command="traffic_manager", select_ports=True)
+
+Test.testName = "Lua states and stats"
+
+# test to ensure origin server works
+request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n",
+                  "timestamp": "1469733493.993", "body": ""}
+response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n",
+                   "timestamp": "1469733493.993", "body": ""}
+
+# add response to the server dictionary
+server.addResponse("sessionfile.log", request_header, response_header)
+
+ts.Disk.remap_config.AddLines({
+    'map / http://127.0.0.1:{}/'.format(server.Variables.Port),
+    'map http://hello http://127.0.0.1:{}/'.format(server.Variables.Port) +
+    ' @plugin=tslua.so @pparam={}/hello.lua'.format(Test.TestDirectory)
+})
+
+ts.Disk.records_config.update({
+    'proxy.config.diags.debug.enabled': 1,
+    'proxy.config.diags.debug.tags': 'ts_lua',
+		'proxy.config.plugin.lua.max_states': 4,
+})
+
+curl_and_args = 'curl -s -D /dev/stdout -o /dev/stderr -x localhost:{} '.format(ts.Variables.port)
+
+# 0 Test - Check for configured lua states
+tr = Test.AddTestRun("Lua states")
+ps = tr.Processes.Default # alias
+ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port))
+ps.StartBefore(Test.Processes.ts, ready=When.PortOpen(ts.Variables.port))
+ps.Command = "traffic_ctl config match lua"
+ps.Env = ts.Env
+ps.ReturnCode = 0
+ps.Streams.stdout.Content = Testers.ContainsExpression("proxy.config.plugin.lua.max_states: 4", "expected 4 states")
+tr.TimeOut = 5
+tr.StillRunningAfter = ts
+
+# 1 Test - Exercise lua script
+tr = Test.AddTestRun("Lua hello")
+ps = tr.Processes.Default # alias
+ps.Command = curl_and_args + ' http://hello/hello'
+ps.TimeOut = 5
+ps.ReturnCode = 0
+ps.Streams.All.Content = Testers.ContainsExpression("Hello, World", "hello world content")
+tr.TimeOut = 5
+tr.StillRunningAfter = ts
+
+# 2 Test - Check for metrics
+tr = Test.AddTestRun("Check for metrics")
+tr.DelayStart = 15 # 5s lag on metrics to update
+ps = tr.Processes.Default # alias
+ps.Command = "traffic_ctl metric match lua"
+ps.Env = ts.Env
+ps.ReturnCode = 0
+ps.Streams.stdout = "gold/metrics.stdout.gold"
+tr.TimeOut = 5
+tr.StillRunningAfter = ts
+
+# 3 Test - Check for developer lifecycle stats
+tr = Test.AddTestRun("Check for lifecycle stats")
+ps = tr.Processes.Default # alias
+ps.Command = "traffic_ctl plugin msg ts_lua print_stats"
+ps.Env = ts.Env
+ps.ReturnCode = 0
+ts.Streams.stderr = "gold/lifecycle.stderr.gold"
+tr.StillRunningAfter = ts