From e8c1a18105bddc29951d4c4b1700dc0af37c83c2 Mon Sep 17 00:00:00 2001
From: simon <simon@lindenlab.com>
Date: Thu, 4 May 2023 16:52:35 +0100
Subject: First work on sl-19676 - Stats on updates

---
 indra/newview/app_settings/settings.xml            |  33 +++
 indra/newview/llappviewer.cpp                      |   7 +-
 indra/newview/llfloater360capture.cpp              |  18 +-
 indra/newview/lltexturefetch.cpp                   |   5 -
 indra/newview/llviewermenu.cpp                     | 143 ++++++-----
 indra/newview/llviewerobjectlist.cpp               |  42 ++--
 indra/newview/llviewerregion.cpp                   |  17 +-
 indra/newview/llviewerregion.h                     |  12 +-
 indra/newview/llviewerstatsrecorder.cpp            | 267 +++++++++++++--------
 indra/newview/llviewerstatsrecorder.h              | 117 +++++----
 indra/newview/skins/default/xui/en/menu_viewer.xml |  24 +-
 11 files changed, 409 insertions(+), 276 deletions(-)

diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml
index 9fffab70f4..3d1a384793 100644
--- a/indra/newview/app_settings/settings.xml
+++ b/indra/newview/app_settings/settings.xml
@@ -17085,5 +17085,38 @@
     <key>Value</key>
     <integer>0</integer>
   </map>
+  <key>StatsReportMaxDuration</key>
+  <map>
+    <key>Comment</key>
+    <string>Maximum seconds for viewer stats file data, prevents huge file</string>
+    <key>Persist</key>
+    <integer>1</integer>
+    <key>Type</key>
+    <string>F32</string>
+    <key>Value</key>
+    <real>300</real>
+  </map>
+	<key>StatsReportFileInterval</key>
+	<map>
+		<key>Comment</key>
+		<string>Interval to save viewer stats file data</string>
+		<key>Persist</key>
+		<integer>1</integer>
+		<key>Type</key>
+		<string>F32</string>
+		<key>Value</key>
+		<real>0.2</real>
+	</map>
+  <key>StatsReportSkipZeroDataSaves</key>
+  <map>
+    <key>Comment</key>
+    <string>In viewer stats data file, skip saving entry if there is no data</string>
+    <key>Persist</key>
+    <integer>0</integer>
+    <key>Type</key>
+    <string>Boolean</string>
+    <key>Value</key>
+    <integer>0</integer>
+  </map>
 </map>
 </llsd>
diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp
index 8d77a39c74..9c194562f9 100644
--- a/indra/newview/llappviewer.cpp
+++ b/indra/newview/llappviewer.cpp
@@ -1536,7 +1536,12 @@ bool LLAppViewer::doFrame()
                     LLFloaterSimpleOutfitSnapshot::update();
                     gGLActive = FALSE;
                 }
-		}
+
+                if (LLViewerStatsRecorder::instanceExists())
+                {
+                    LLViewerStatsRecorder::instance().idle();
+                }
+            }
 		}
 
 		{
diff --git a/indra/newview/llfloater360capture.cpp b/indra/newview/llfloater360capture.cpp
index 23f86e2361..9c25cdbc7d 100644
--- a/indra/newview/llfloater360capture.cpp
+++ b/indra/newview/llfloater360capture.cpp
@@ -197,21 +197,18 @@ void LLFloater360Capture::changeInterestListMode(bool send_everything)
 
     if (gAgent.requestPostCapability("InterestList", body, [](const LLSD & response)
     {
-        LL_INFOS("360Capture") <<
-                               "InterestList capability responded: \n" <<
+        LL_DEBUGS("360Capture") << "InterestList capability responded: \n" <<
                                ll_pretty_print_sd(response) <<
                                LL_ENDL;
     }))
     {
-        LL_INFOS("360Capture") <<
-                               "Successfully posted an InterestList capability request with payload: \n" <<
+        LL_DEBUGS("360Capture") << "Successfully posted an InterestList capability request with payload: \n" <<
                                ll_pretty_print_sd(body) <<
                                LL_ENDL;
     }
     else
     {
-        LL_INFOS("360Capture") <<
-                               "Unable to post an InterestList capability request with payload: \n" <<
+        LL_WARNS("360Capture") << "Unable to post an InterestList capability request with payload: \n" <<
                                ll_pretty_print_sd(body) <<
                                LL_ENDL;
     }
@@ -632,11 +629,8 @@ void LLFloater360Capture::capture360Images()
     // display time to encode all 6 images.  It tends to be a fairly linear
     // time for each so we don't need to worry about displaying the time
     // for each - this gives us plenty to use for optimizing
-    LL_INFOS("360Capture") <<
-                           "Time to encode and save 6 images was " <<
-                           encode_time_total <<
-                           " seconds" <<
-                           LL_ENDL;
+    LL_INFOS("360Capture") << "Time to encode and save 6 images was " <<
+                           encode_time_total << " seconds" << LL_ENDL;
 
     // Write the JavaScript file footer (the bottom of the file after the
     // declarations of the actual data URLs array). The footer comprises of
@@ -668,7 +662,7 @@ void LLFloater360Capture::capture360Images()
     // as a change - only the subsequent 5 are
     if (camera_changed_times < 5)
     {
-        LL_INFOS("360Capture") << "Warning: we only captured " << camera_changed_times << " images." << LL_ENDL;
+        LL_WARNS("360Capture") << "360 image capture expected 5 or more images, only captured " << camera_changed_times << " images." << LL_ENDL;
     }
 
     // now we have the 6 shots saved in a well specified location,
diff --git a/indra/newview/lltexturefetch.cpp b/indra/newview/lltexturefetch.cpp
index a7dcb1a9bb..b0d6a5d466 100644
--- a/indra/newview/lltexturefetch.cpp
+++ b/indra/newview/lltexturefetch.cpp
@@ -2258,11 +2258,6 @@ S32 LLTextureFetchWorker::callbackHttpGet(LLCore::HttpResponse * response,
 	mLoaded = TRUE;
 	setPriority(LLWorkerThread::PRIORITY_HIGH | mWorkPriority);
 
-	if (LLViewerStatsRecorder::instanceExists())
-	{
-		// Do not create this instance inside thread
-		LLViewerStatsRecorder::instance().log(0.2f);
-	}
 	return data_size ;
 }
 
diff --git a/indra/newview/llviewermenu.cpp b/indra/newview/llviewermenu.cpp
index 998f5c020e..77cb5f86af 100644
--- a/indra/newview/llviewermenu.cpp
+++ b/indra/newview/llviewermenu.cpp
@@ -119,6 +119,7 @@
 #include "llviewerobjectlist.h"
 #include "llviewerparcelmgr.h"
 #include "llviewerstats.h"
+#include "llviewerstatsrecorder.h"
 #include "llvoavatarself.h"
 #include "llvoicevivox.h"
 #include "llworldmap.h"
@@ -1295,49 +1296,99 @@ class LLAdvancedDumpRegionObjectCache : public view_listener_t
 	}
 };
 
-class LLAdvancedInterestListFullUpdate : public view_listener_t
+class LLAdvancedToggleInterestList360Mode : public view_listener_t
 {
-	bool handleEvent(const LLSD& userdata)
-	{
-		LLSD request;
-		LLSD body;
-		static bool using_360 = false;
-
-		if (using_360)
-		{
-			body["mode"] = LLSD::String("default");
-		}
-		else
-		{
-			body["mode"] = LLSD::String("360");
-		}
-		using_360 = !using_360;
+public:
+    bool handleEvent(const LLSD &userdata)
+    {
+        LLSD request;
+        LLSD body;
+
+        // First do a GET to report on current mode and update stats
+        if (gAgent.requestGetCapability("InterestList",
+                                        [](const LLSD &response) {
+                                            LL_DEBUGS("360Capture") << "InterestList capability GET responded: \n"
+                                                                   << ll_pretty_print_sd(response) << LL_ENDL;
+                                        }))
+        {
+            LL_DEBUGS("360Capture") << "Successful GET InterestList capability request with return body: \n"
+                                   << ll_pretty_print_sd(body) << LL_ENDL;
+        }
+        else
+        {
+            LL_WARNS("360Capture") << "Unable to GET InterestList capability request with return body: \n"
+                                   << ll_pretty_print_sd(body) << LL_ENDL;
+        }
 
-        if (gAgent.requestPostCapability("InterestList", body, [](const LLSD& response)
+        // Now do a POST to change the mode
+        if (sUsing360)
         {
-            LL_INFOS("360Capture") <<
-                "InterestList capability responded: \n" <<
-                ll_pretty_print_sd(response) <<
-                LL_ENDL;
-        }))
+            body["mode"] = LLSD::String("default");
+        }
+        else
         {
-            LL_INFOS("360Capture") <<
-                "Successfully posted an InterestList capability request with payload: \n" <<
-                ll_pretty_print_sd(body) <<
-                LL_ENDL;
+            body["mode"] = LLSD::String("360");
+        }
+        sUsing360 = !sUsing360;
+        LL_INFOS("360Capture") << "Setting InterestList capability mode to " << body["mode"].asString() << LL_ENDL;
+
+        if (gAgent.requestPostCapability("InterestList", body,
+                                         [](const LLSD &response) {
+                                             LL_DEBUGS("360Capture") << "InterestList capability responded: \n"
+                                                                    << ll_pretty_print_sd(response) << LL_ENDL;
+                                         }))
+        {
+            LL_DEBUGS("360Capture") << "Successfully posted an InterestList capability request with payload: \n"
+                                   << ll_pretty_print_sd(body) << LL_ENDL;
             return true;
         }
         else
         {
-            LL_INFOS("360Capture") <<
-                "Unable to post an InterestList capability request with payload: \n" <<
-                ll_pretty_print_sd(body) <<
-                LL_ENDL;
+            LL_DEBUGS("360Capture") << "Unable to post an InterestList capability request with payload: \n"
+                                   << ll_pretty_print_sd(body) << LL_ENDL;
             return false;
         }
+    };
+
+	static bool sUsing360;
+};
+
+bool LLAdvancedToggleInterestList360Mode::sUsing360 = false;
+
+class LLAdvancedCheckInterestList360Mode : public view_listener_t
+{
+	bool handleEvent(const LLSD& userdata)
+	{
+		return LLAdvancedToggleInterestList360Mode::sUsing360;
 	}
 };
 
+class LLAdvancedToggleStatsRecorder : public view_listener_t
+{
+    bool handleEvent(const LLSD &userdata)
+    {
+        if (LLViewerStatsRecorder::instance().isEnabled())
+		{	// Turn off both recording and logging
+			LLViewerStatsRecorder::instance().enableObjectStatsRecording(false);
+		}
+		else
+		{	// Turn on both recording and logging
+			LLViewerStatsRecorder::instance().enableObjectStatsRecording(true, true);
+		}
+        return true;
+    }
+};
+
+class LLAdvancedCheckStatsRecorder : public view_listener_t
+{
+    bool handleEvent(const LLSD &userdata)
+    {	// Use the logging state as the indicator of whether the stats recorder is on
+        return LLViewerStatsRecorder::instance().isLogging();
+    }
+};
+
+
+
 class LLAdvancedBuyCurrencyTest : public view_listener_t
 	{
 	bool handleEvent(const LLSD& userdata)
@@ -4492,33 +4543,6 @@ void handle_duplicate_in_place(void*)
 	LLSelectMgr::getInstance()->selectDuplicate(offset, TRUE);
 }
 
-/* dead code 30-apr-2008
-void handle_deed_object_to_group(void*)
-{
-	LLUUID group_id;
-	
-	LLSelectMgr::getInstance()->selectGetGroup(group_id);
-	LLSelectMgr::getInstance()->sendOwner(LLUUID::null, group_id, FALSE);
-	LLViewerStats::getInstance()->incStat(LLViewerStats::ST_RELEASE_COUNT);
-}
-
-BOOL enable_deed_object_to_group(void*)
-{
-	if(LLSelectMgr::getInstance()->getSelection()->isEmpty()) return FALSE;
-	LLPermissions perm;
-	LLUUID group_id;
-
-	if (LLSelectMgr::getInstance()->selectGetGroup(group_id) &&
-		gAgent.hasPowerInGroup(group_id, GP_OBJECT_DEED) &&
-		LLSelectMgr::getInstance()->selectGetPermissions(perm) &&
-		perm.deedToGroup(gAgent.getID(), group_id))
-	{
-		return TRUE;
-	}
-	return FALSE;
-}
-
-*/
 
 
 /*
@@ -9448,7 +9472,10 @@ void initialize_menus()
 	// Advanced > World
 	view_listener_t::addMenu(new LLAdvancedDumpScriptedCamera(), "Advanced.DumpScriptedCamera");
 	view_listener_t::addMenu(new LLAdvancedDumpRegionObjectCache(), "Advanced.DumpRegionObjectCache");
-	view_listener_t::addMenu(new LLAdvancedInterestListFullUpdate(), "Advanced.InterestListFullUpdate");
+    view_listener_t::addMenu(new LLAdvancedToggleInterestList360Mode(), "Advanced.ToggleInterestList360Mode");
+    view_listener_t::addMenu(new LLAdvancedCheckInterestList360Mode(), "Advanced.CheckInterestList360Mode");
+    view_listener_t::addMenu(new LLAdvancedToggleStatsRecorder(), "Advanced.ToggleStatsRecorder");
+    view_listener_t::addMenu(new LLAdvancedCheckStatsRecorder(), "Advanced.CheckStatsRecorder");
 
 	// Advanced > UI
 	commit.add("Advanced.WebBrowserTest", boost::bind(&handle_web_browser_test,	_2));	// sigh! this one opens the MEDIA browser
diff --git a/indra/newview/llviewerobjectlist.cpp b/indra/newview/llviewerobjectlist.cpp
index efc4ded79e..0c9e929cf3 100644
--- a/indra/newview/llviewerobjectlist.cpp
+++ b/indra/newview/llviewerobjectlist.cpp
@@ -369,7 +369,7 @@ LLViewerObject* LLViewerObjectList::processObjectUpdateFromCache(LLVOCacheEntry*
 		if (!objectp)
 		{
 			LL_INFOS() << "createObject failure for object: " << fullid << LL_ENDL;
-			recorder.objectUpdateFailure(entry->getLocalID(), OUT_FULL_CACHED, 0);
+			recorder.objectUpdateFailure(0);
 			return NULL;
 		}
 		justCreated = true;
@@ -393,7 +393,6 @@ LLViewerObject* LLViewerObjectList::processObjectUpdateFromCache(LLVOCacheEntry*
 		objectp->setLastUpdateType(OUT_FULL_COMPRESSED); //newly cached
 		objectp->setLastUpdateCached(TRUE);
 	}
-	recorder.log(0.2f);
 	LLVOAvatar::cullAvatarsByPixelArea();
 
 	return objectp;
@@ -472,18 +471,15 @@ void LLViewerObjectList::processObjectUpdate(LLMessageSystem *mesgsys,
 
 	for (i = 0; i < num_objects; i++)
 	{
-		// timer is unused?
-		LLTimer update_timer;
 		BOOL justCreated = FALSE;
 		S32	msg_size = 0;
 		bool update_cache = false; //update object cache if it is a full-update or terse update
 
 		if (compressed)
 		{
-			S32							uncompressed_length = 2048;
 			compressed_dp.reset();
 
-			uncompressed_length = mesgsys->getSizeFast(_PREHASH_ObjectData, i, _PREHASH_Data);
+			S32 uncompressed_length = mesgsys->getSizeFast(_PREHASH_ObjectData, i, _PREHASH_Data);
 			LL_DEBUGS("ObjectUpdate") << "got binary data from message to compressed_dpbuffer" << LL_ENDL;
 			mesgsys->getBinaryDataFast(_PREHASH_ObjectData, _PREHASH_Data, compressed_dpbuffer, 0, i, 2048);
 			compressed_dp.assignBuffer(compressed_dpbuffer, uncompressed_length);
@@ -505,7 +501,7 @@ void LLViewerObjectList::processObjectUpdate(LLMessageSystem *mesgsys,
 						<< " Flags: " << flags
 						<< " Region: " << regionp->getName()
 						<< " Region id: " << regionp->getRegionID() << LL_ENDL;
-					recorder.objectUpdateFailure(local_id, update_type, msg_size);
+					recorder.objectUpdateFailure(msg_size);
 					continue;
 				}
 				else if ((flags & FLAGS_TEMPORARY_ON_REZ) == 0)
@@ -616,7 +612,7 @@ void LLViewerObjectList::processObjectUpdate(LLMessageSystem *mesgsys,
 				if (update_type == OUT_TERSE_IMPROVED)
 				{
 					// LL_INFOS() << "terse update for an unknown object (compressed):" << fullid << LL_ENDL;
-					recorder.objectUpdateFailure(local_id, update_type, msg_size);
+					recorder.objectUpdateFailure(msg_size);
 					continue;
 				}
 			}
@@ -625,7 +621,7 @@ void LLViewerObjectList::processObjectUpdate(LLMessageSystem *mesgsys,
 				if (update_type != OUT_FULL)
 				{
 					//LL_INFOS() << "terse update for an unknown object:" << fullid << LL_ENDL;
-					recorder.objectUpdateFailure(local_id, update_type, msg_size);
+					recorder.objectUpdateFailure(msg_size);
 					continue;
 				}
 
@@ -638,7 +634,7 @@ void LLViewerObjectList::processObjectUpdate(LLMessageSystem *mesgsys,
 			{
 				mNumDeadObjectUpdates++;
 				//LL_INFOS() << "update for a dead object:" << fullid << LL_ENDL;
-				recorder.objectUpdateFailure(local_id, update_type, msg_size);
+				recorder.objectUpdateFailure(msg_size);
 				continue;
 			}
 #endif
@@ -651,7 +647,7 @@ void LLViewerObjectList::processObjectUpdate(LLMessageSystem *mesgsys,
 			if (!objectp)
 			{
 				LL_INFOS() << "createObject failure for object: " << fullid << LL_ENDL;
-				recorder.objectUpdateFailure(local_id, update_type, msg_size);
+				recorder.objectUpdateFailure(msg_size);
 				continue;
 			}
 
@@ -681,11 +677,11 @@ void LLViewerObjectList::processObjectUpdate(LLMessageSystem *mesgsys,
 			
 				if(!(flags & FLAGS_TEMPORARY_ON_REZ))
 				{
-				bCached = true;
+					bCached = true;
 					LLViewerRegion::eCacheUpdateResult result = objectp->mRegionp->cacheFullUpdate(objectp, compressed_dp, flags);
-				recorder.cacheFullUpdate(local_id, update_type, result, objectp, msg_size);
+					recorder.cacheFullUpdate(result);
+				}
 			}
-		}
 #endif
 		}
 		else
@@ -696,12 +692,10 @@ void LLViewerObjectList::processObjectUpdate(LLMessageSystem *mesgsys,
 			}
 			processUpdateCore(objectp, user_data, i, update_type, NULL, justCreated);
 		}
-		recorder.objectUpdateEvent(local_id, update_type, objectp, msg_size);
+		recorder.objectUpdateEvent(update_type);
 		objectp->setLastUpdateType(update_type);
 	}
 
-	recorder.log(0.2f);
-
 	LLVOAvatar::cullAvatarsByPixelArea();
 }
 
@@ -748,14 +742,14 @@ void LLViewerObjectList::processCachedObjectUpdate(LLMessageSystem *mesgsys,
 
 		// Lookup data packer and add this id to cache miss lists if necessary.
 		U8 cache_miss_type = LLViewerRegion::CACHE_MISS_TYPE_NONE;
-		if(!regionp->probeCache(id, crc, flags, cache_miss_type))
-		{
-			// Cache Miss.
+        if (regionp->probeCache(id, crc, flags, cache_miss_type))
+		{	// Cache Hit
+            recorder.cacheHitEvent();
+		}
+        else
+		{	// Cache Miss
             LL_DEBUGS("ObjectUpdate") << "cache miss for id " << id << " crc " << crc << " miss type " << (S32) cache_miss_type << LL_ENDL;
-
-			recorder.cacheMissEvent(id, update_type, cache_miss_type, msg_size);
-
-			continue; // no data packer, skip this object
+            recorder.cacheMissEvent(cache_miss_type);
 		}
 	}
 
diff --git a/indra/newview/llviewerregion.cpp b/indra/newview/llviewerregion.cpp
index ad7321ca4b..3d1a81694a 100755
--- a/indra/newview/llviewerregion.cpp
+++ b/indra/newview/llviewerregion.cpp
@@ -2618,14 +2618,10 @@ LLVOCacheEntry* LLViewerRegion::getCacheEntry(U32 local_id, bool valid)
 	return NULL;
 	}
 
-void LLViewerRegion::addCacheMiss(U32 id, LLViewerRegion::eCacheMissType miss_type)
+void LLViewerRegion::addCacheMiss(U32 id, LLViewerRegion::eCacheMissType cache_miss_type)
 {
 	mRegionCacheMissCount++;
-#if 0
-	mCacheMissList.insert(CacheMissItem(id, miss_type));
-#else
-	mCacheMissList.push_back(CacheMissItem(id, miss_type));
-#endif
+    mCacheMissList.push_back(CacheMissItem(id, cache_miss_type));
 }
 
 //check if a non-cacheable object is already created.
@@ -2701,10 +2697,10 @@ bool LLViewerRegion::probeCache(U32 local_id, U32 crc, U32 flags, U8 &cache_miss
 		}
 	}
 	else
-	{
+	{	// Total miss, don't have the object in cache
 		// LL_INFOS() << "Cache miss for " << local_id << LL_ENDL;
-		addCacheMiss(local_id, CACHE_MISS_TYPE_FULL);
-        cache_miss_type = CACHE_MISS_TYPE_FULL;
+        addCacheMiss(local_id, CACHE_MISS_TYPE_TOTAL);
+        cache_miss_type = CACHE_MISS_TYPE_TOTAL;
 	}
 
 	return false;
@@ -2712,7 +2708,7 @@ bool LLViewerRegion::probeCache(U32 local_id, U32 crc, U32 flags, U8 &cache_miss
 
 void LLViewerRegion::addCacheMissFull(const U32 local_id)
 {
-	addCacheMiss(local_id, CACHE_MISS_TYPE_FULL);
+	addCacheMiss(local_id, CACHE_MISS_TYPE_TOTAL);
 }
 
 void LLViewerRegion::requestCacheMisses()
@@ -2763,7 +2759,6 @@ void LLViewerRegion::requestCacheMisses()
 	mCacheDirty = TRUE ;
 	// LL_INFOS() << "KILLDEBUG Sent cache miss full " << full_count << " crc " << crc_count << LL_ENDL;
 	LLViewerStatsRecorder::instance().requestCacheMissesEvent(mCacheMissList.size());
-	LLViewerStatsRecorder::instance().log(0.2f);
 
 	mCacheMissList.clear();
 }
diff --git a/indra/newview/llviewerregion.h b/indra/newview/llviewerregion.h
index 6548e8d372..81371b7f30 100644
--- a/indra/newview/llviewerregion.h
+++ b/indra/newview/llviewerregion.h
@@ -332,9 +332,9 @@ public:
 
 	typedef enum
 	{
-		CACHE_MISS_TYPE_FULL = 0,
-		CACHE_MISS_TYPE_CRC,
-		CACHE_MISS_TYPE_NONE
+		CACHE_MISS_TYPE_TOTAL = 0,	// total cache miss - object not in cache
+		CACHE_MISS_TYPE_CRC,		// object in cache, but CRC doesn't match
+		CACHE_MISS_TYPE_NONE		// not a miss:  cache hit
 	} eCacheMissType;
 
 	typedef enum
@@ -551,10 +551,10 @@ private:
 	class CacheMissItem
 	{
 	public:
-		CacheMissItem(U32 id, LLViewerRegion::eCacheMissType miss_type) : mID(id), mType(miss_type){}
+        CacheMissItem(U32 id, LLViewerRegion::eCacheMissType miss_type) : mID(id), mType(miss_type) {}
 
-		U32                            mID;     //local object id
-		LLViewerRegion::eCacheMissType mType;   //cache miss type
+		U32                         mID;     //local object id
+        LLViewerRegion::eCacheMissType	mType;  // cache miss type
 
 		typedef std::list<CacheMissItem> cache_miss_list_t;
 	};
diff --git a/indra/newview/llviewerstatsrecorder.cpp b/indra/newview/llviewerstatsrecorder.cpp
index b5ccf4ffa0..64167135ac 100644
--- a/indra/newview/llviewerstatsrecorder.cpp
+++ b/indra/newview/llviewerstatsrecorder.cpp
@@ -28,24 +28,25 @@
 #include "llviewerstatsrecorder.h"
 
 
+#include "llcontrol.h"
 #include "llfile.h"
 #include "llviewerregion.h"
 #include "llviewerobject.h"
+#include "llworld.h"
 
-
-// To do - something using region name or global position
-#if LL_WINDOWS
-	static const std::string STATS_FILE_NAME("C:\\ViewerObjectCacheStats.csv");
-#else
-	static const std::string STATS_FILE_NAME("/tmp/viewerstats.csv");
-#endif
+extern LLControlGroup  gSavedSettings;
 
 LLViewerStatsRecorder* LLViewerStatsRecorder::sInstance = NULL;
 LLViewerStatsRecorder::LLViewerStatsRecorder() :
-	mObjectCacheFile(NULL),
+	mStatsFile(NULL),
 	mTimer(),
-	mStartTime(0.0),
-	mLastSnapshotTime(0.0)
+	mFileOpenTime(0.0),
+	mLastSnapshotTime(0.0),
+    mEnableStatsRecording(false),
+    mEnableStatsLogging(false),
+    mInterval(0.2),
+    mMaxDuration(300.f),
+    mSkipSaveIfZeros(false)
 {
 	if (NULL != sInstance)
 	{
@@ -57,30 +58,22 @@ LLViewerStatsRecorder::LLViewerStatsRecorder() :
 
 LLViewerStatsRecorder::~LLViewerStatsRecorder()
 {
-	if (mObjectCacheFile != NULL)
+	if (mStatsFile)
 	{
-		// last chance snapshot
-		writeToLog(0.f);
-		LLFile::close(mObjectCacheFile);
-		mObjectCacheFile = NULL;
+        writeToLog(0.f);  // Save last data
+        closeStatsFile();
 	}
 }
 
 void LLViewerStatsRecorder::clearStats()
 {
 	mObjectCacheHitCount = 0;
-	mObjectCacheHitSize = 0;
 	mObjectCacheMissFullCount = 0;
-	mObjectCacheMissFullSize = 0;
 	mObjectCacheMissCrcCount = 0;
-	mObjectCacheMissCrcSize = 0;
 	mObjectFullUpdates = 0;
-	mObjectFullUpdatesSize = 0;
 	mObjectTerseUpdates = 0;
-	mObjectTerseUpdatesSize = 0;
 	mObjectCacheMissRequests = 0;
 	mObjectCacheMissResponses = 0;
-	mObjectCacheMissResponsesSize = 0;
 	mObjectCacheUpdateDupes = 0;
 	mObjectCacheUpdateChanges = 0;
 	mObjectCacheUpdateAdds = 0;
@@ -91,45 +84,57 @@ void LLViewerStatsRecorder::clearStats()
 }
 
 
-void LLViewerStatsRecorder::recordObjectUpdateFailure(U32 local_id, const EObjectUpdateType update_type, S32 msg_size)
+void LLViewerStatsRecorder::enableObjectStatsRecording(bool enable, bool logging /* false */)
+{
+    mEnableStatsRecording = enable;
+
+	// if logging is stopping, close the file
+    if (mStatsFile && !logging)
+	{
+        writeToLog(0.f);  // Save last data
+        closeStatsFile();
+	}
+    mEnableStatsLogging = logging;
+}
+
+
+
+void LLViewerStatsRecorder::recordObjectUpdateFailure(S32 msg_size)
 {
 	mObjectUpdateFailures++;
 	mObjectUpdateFailuresSize += msg_size;
 }
 
-void LLViewerStatsRecorder::recordCacheMissEvent(U32 local_id, const EObjectUpdateType update_type, U8 cache_miss_type, S32 msg_size)
+void LLViewerStatsRecorder::recordCacheMissEvent(U8 cache_miss_type)
 {
-	if (LLViewerRegion::CACHE_MISS_TYPE_FULL == cache_miss_type)
+	if (LLViewerRegion::CACHE_MISS_TYPE_TOTAL == cache_miss_type)
 	{
 		mObjectCacheMissFullCount++;
-		mObjectCacheMissFullSize += msg_size;
 	}
 	else
 	{
 		mObjectCacheMissCrcCount++;
-		mObjectCacheMissCrcSize += msg_size;
 	}
 }
 
-void LLViewerStatsRecorder::recordObjectUpdateEvent(U32 local_id, const EObjectUpdateType update_type, LLViewerObject * objectp, S32 msg_size)
+
+void LLViewerStatsRecorder::recordCacheHitEvent()
+{
+    mObjectCacheHitCount++;
+}
+
+void LLViewerStatsRecorder::recordObjectUpdateEvent(const EObjectUpdateType update_type)
 {
-	switch (update_type)
+    switch (update_type)
 	{
 	case OUT_FULL:
 		mObjectFullUpdates++;
-		mObjectFullUpdatesSize += msg_size;
 		break;
 	case OUT_TERSE_IMPROVED:
 		mObjectTerseUpdates++;
-		mObjectTerseUpdatesSize += msg_size;
 		break;
 	case OUT_FULL_COMPRESSED:
 		mObjectCacheMissResponses++;
-		mObjectCacheMissResponsesSize += msg_size;
-		break;
-	case OUT_FULL_CACHED:
-		mObjectCacheHitCount++;
-		mObjectCacheHitSize += msg_size;
 		break;
 	default:
 		LL_WARNS() << "Unknown update_type" << LL_ENDL;
@@ -137,9 +142,9 @@ void LLViewerStatsRecorder::recordObjectUpdateEvent(U32 local_id, const EObjectU
 	};
 }
 
-void LLViewerStatsRecorder::recordCacheFullUpdate(U32 local_id, const EObjectUpdateType update_type, LLViewerRegion::eCacheUpdateResult update_result, LLViewerObject* objectp, S32 msg_size)
+void LLViewerStatsRecorder::recordCacheFullUpdate(LLViewerRegion::eCacheUpdateResult update_result)
 {
-	switch (update_result)
+    switch (update_result)
 	{
 		case LLViewerRegion::CACHE_UPDATE_DUPE:
 			mObjectCacheUpdateDupes++;
@@ -154,7 +159,7 @@ void LLViewerStatsRecorder::recordCacheFullUpdate(U32 local_id, const EObjectUpd
 			mObjectCacheUpdateReplacements++;
 			break;
 		default:
-			LL_WARNS() << "Unknown update_result type" << LL_ENDL;
+            LL_WARNS() << "Unknown update_result type " << (S32) update_result << LL_ENDL;
 			break;
 	};
 }
@@ -166,14 +171,30 @@ void LLViewerStatsRecorder::recordRequestCacheMissesEvent(S32 count)
 
 void LLViewerStatsRecorder::writeToLog( F32 interval )
 {
+    if (!mEnableStatsLogging || !mEnableStatsRecording)
+	{
+		return;
+	}
+
 	size_t data_size = 0;
-	F64 delta_time = LLTimer::getTotalSeconds() - mLastSnapshotTime;
-	S32 total_objects = mObjectCacheHitCount + mObjectCacheMissCrcCount + mObjectCacheMissFullCount + mObjectFullUpdates + mObjectTerseUpdates + mObjectCacheMissRequests + mObjectCacheMissResponses + mObjectCacheUpdateDupes + mObjectCacheUpdateChanges + mObjectCacheUpdateAdds + mObjectCacheUpdateReplacements + mObjectUpdateFailures;
+	F64 delta_time = LLFrameTimer::getTotalSeconds() - mLastSnapshotTime;
+    if (delta_time < interval)
+        return;
 
-	if ( delta_time < interval || total_objects == 0) return;
+	if (mSkipSaveIfZeros)
+	{
+        S32 total_events = mObjectCacheHitCount + mObjectCacheMissCrcCount + mObjectCacheMissFullCount + mObjectFullUpdates +
+                            mObjectTerseUpdates + mObjectCacheMissRequests + mObjectCacheMissResponses + mObjectCacheUpdateDupes +
+                            mObjectCacheUpdateChanges + mObjectCacheUpdateAdds + mObjectCacheUpdateReplacements + mObjectUpdateFailures;
+		if (total_events == 0)
+		{
+            LL_DEBUGS("ILXZeroData") << "ILX: not saving zero data" << LL_ENDL;
+            return;
+        }
+	}
 
-	mLastSnapshotTime = LLTimer::getTotalSeconds();
-	LL_DEBUGS() << "ILX: " 
+	mLastSnapshotTime = LLFrameTimer::getTotalSeconds();
+    LL_DEBUGS("ILX") << "ILX: "
 		<< mObjectCacheHitCount << " hits, " 
 		<< mObjectCacheMissFullCount << " full misses, "
 		<< mObjectCacheMissCrcCount << " crc misses, "
@@ -188,84 +209,124 @@ void LLViewerStatsRecorder::writeToLog( F32 interval )
 		<< mObjectUpdateFailures << " update failures"
 		<< LL_ENDL;
 
-	if (mObjectCacheFile == NULL)
+	if (mStatsFile == NULL)
 	{
-		mStartTime = LLTimer::getTotalSeconds();
-		mObjectCacheFile = LLFile::fopen(STATS_FILE_NAME, "wb");
-		if (mObjectCacheFile)
-		{	// Write column headers
-			std::ostringstream data_msg;
-			data_msg << "EventTime(ms)\t"
-				<< "Cache Hits\t"
-				<< "Cache Full Misses\t"
-				<< "Cache Crc Misses\t"
-				<< "Full Updates\t"
-				<< "Terse Updates\t"
-				<< "Cache Miss Requests\t"
-				<< "Cache Miss Responses\t"
-				<< "Cache Update Dupes\t"
-				<< "Cache Update Changes\t"
-				<< "Cache Update Adds\t"
-				<< "Cache Update Replacements\t"
-				<< "Update Failures\t"
-				<< "Cache Hits bps\t"
-				<< "Cache Full Misses bps\t"
-				<< "Cache Crc Misses bps\t"
-				<< "Full Updates bps\t"
-				<< "Terse Updates bps\t"
-				<< "Cache Miss Responses bps\t"
-				<< "Texture Fetch bps\t"
-				<< "\n";
-
-			data_size = data_msg.str().size();
-			if (fwrite(data_msg.str().c_str(), 1, data_size, mObjectCacheFile ) != data_size)
+		// Refresh settings
+        mInterval        = gSavedSettings.getF32("StatsReportFileInterval");
+        mSkipSaveIfZeros = gSavedSettings.getBOOL("StatsReportSkipZeroDataSaves");
+        mMaxDuration	 = gSavedSettings.getF32("StatsReportMaxDuration");
+
+		// Open the data file
+        makeStatsFileName();
+        mStatsFile = LLFile::fopen(mStatsFileName, "wb");
+
+		if (mStatsFile)
+		{
+            LL_INFOS("ILX") << "ILX: Writing update information to " << mStatsFileName << LL_ENDL;
+
+            mFileOpenTime = LLFrameTimer::getTotalSeconds();
+
+            // Write column headers
+			std::ostringstream col_headers;
+			col_headers << "Time (sec),"
+					<< "Regions,"
+					<< "Active Cached Objects,"
+					<< "Cache Hits,"
+					<< "Cache Full Misses,"
+					<< "Cache Crc Misses,"
+					<< "Full Updates,"
+					<< "Terse Updates,"
+					<< "Cache Miss Requests,"
+					<< "Cache Miss Responses,"
+					<< "Cache Update Dupes,"
+					<< "Cache Update Changes,"
+					<< "Cache Update Adds,"
+					<< "Cache Update Replacements,"
+					<< "Update Failures,"
+					<< "Texture Fetch bps,"
+					<< "\n";
+
+			data_size = col_headers.str().size();
+			if (fwrite(col_headers.str().c_str(), 1, data_size, mStatsFile ) != data_size)
 			{
-				LL_WARNS() << "failed to write full headers to " << STATS_FILE_NAME << LL_ENDL;
+				LL_WARNS() << "failed to write full headers to " << mStatsFileName << LL_ENDL;
 			}
 		}
 		else
-		{
-			//LL_WARNS() << "Couldn't open " << STATS_FILE_NAME << " for logging." << LL_ENDL;
+		{	// Failed to open file
+			LL_WARNS() << "Couldn't open " << mStatsFileName << " for logging, turning off stats recording." << LL_ENDL;
+            mEnableStatsLogging = false;
 			return;
 		}
 	}
 
-	std::ostringstream data_msg;
-
-	data_msg << getTimeSinceStart()
-		<< "\t " << mObjectCacheHitCount
-		<< "\t" << mObjectCacheMissFullCount
-		<< "\t" << mObjectCacheMissCrcCount
-		<< "\t" << mObjectFullUpdates
-		<< "\t" << mObjectTerseUpdates
-		<< "\t" << mObjectCacheMissRequests
-		<< "\t" << mObjectCacheMissResponses
-		<< "\t" << mObjectCacheUpdateDupes
-		<< "\t" << mObjectCacheUpdateChanges
-		<< "\t" << mObjectCacheUpdateAdds
-		<< "\t" << mObjectCacheUpdateReplacements
-		<< "\t" << mObjectUpdateFailures
-		<< "\t" << (mObjectCacheHitSize * 8 / delta_time)
-		<< "\t" << (mObjectCacheMissFullSize * 8 / delta_time)
-		<< "\t" << (mObjectCacheMissCrcSize * 8 / delta_time)
-		<< "\t" << (mObjectFullUpdatesSize * 8 / delta_time)
-		<< "\t" << (mObjectTerseUpdatesSize * 8 / delta_time)
-		<< "\t" << (mObjectCacheMissResponsesSize * 8 / delta_time)
-		<< "\t" << (mTextureFetchSize * 8 / delta_time)
+	std::ostringstream stats_data;
+
+	stats_data << getTimeSinceStart()
+		<< "," << LLWorld::getInstance()->getRegionList().size()
+		<< "," << LLWorld::getInstance()->getNumOfActiveCachedObjects()
+		<< "," << mObjectCacheHitCount
+		<< "," << mObjectCacheMissFullCount
+		<< "," << mObjectCacheMissCrcCount
+		<< "," << mObjectFullUpdates
+		<< "," << mObjectTerseUpdates
+		<< "," << mObjectCacheMissRequests
+		<< "," << mObjectCacheMissResponses
+		<< "," << mObjectCacheUpdateDupes
+		<< "," << mObjectCacheUpdateChanges
+		<< "," << mObjectCacheUpdateAdds
+		<< "," << mObjectCacheUpdateReplacements
+		<< "," << mObjectUpdateFailures
+		<< "," << (mTextureFetchSize * 8 / delta_time)
 		<< "\n";
 
-	data_size = data_msg.str().size();
-	if ( data_size != fwrite(data_msg.str().c_str(), 1, data_size, mObjectCacheFile ))
+	data_size = stats_data.str().size();
+	if ( data_size != fwrite(stats_data.str().c_str(), 1, data_size, mStatsFile ))
 	{
-				LL_WARNS() << "Unable to write complete column data to " << STATS_FILE_NAME << LL_ENDL;
-	}
+		LL_WARNS() << "Unable to write complete column data to " << mStatsFileName << LL_ENDL;
+        closeStatsFile();
+    }
 
 	clearStats();
+
+	if (getTimeSinceStart() >= mMaxDuration)
+    {  // If file recording has been running for too long, stop it.
+        closeStatsFile();
+    }
+}
+
+void LLViewerStatsRecorder::closeStatsFile()
+{
+	if (mStatsFile)
+	{
+        LL_INFOS("ILX") << "ILX: Stopped writing update information to " << mStatsFileName << " after " << getTimeSinceStart()
+                        << " seconds." << LL_ENDL;
+        LLFile::close(mStatsFile);
+		mStatsFile = NULL;
+    }
+    mEnableStatsLogging = false;
+}
+
+void LLViewerStatsRecorder::makeStatsFileName()
+{
+    // Create filename - tbd: use pid?
+#if LL_WINDOWS
+    std::string stats_file_name("SLViewerStats-");
+#else
+    std::string stats_file_name("slviewerstats-");
+#endif
+
+    F64         now      = LLFrameTimer::getTotalSeconds();
+    std::string date_str = LLDate(now).asString();
+    std::replace(date_str.begin(), date_str.end(), ':', '-');  // Make it valid for a filename
+    stats_file_name.append(date_str);
+    stats_file_name.append(".csv");
+    mStatsFileName = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, stats_file_name);
 }
 
 F32 LLViewerStatsRecorder::getTimeSinceStart()
 {
-	return (F32) (LLTimer::getTotalSeconds() - mStartTime);
+	return (F32) (LLFrameTimer::getTotalSeconds() - mFileOpenTime);
 }
 
 void LLViewerStatsRecorder::recordTextureFetch( S32 msg_size )
diff --git a/indra/newview/llviewerstatsrecorder.h b/indra/newview/llviewerstatsrecorder.h
index c974bea49d..90f8ca8742 100644
--- a/indra/newview/llviewerstatsrecorder.h
+++ b/indra/newview/llviewerstatsrecorder.h
@@ -31,10 +31,6 @@
 // This is a diagnostic class used to record information from the viewer
 // for analysis.
 
-// This is normally 0.  Set to 1 to enable viewer stats recording
-#define LL_RECORD_VIEWER_STATS	0
-
-
 #include "llframetimer.h"
 #include "llviewerobject.h"
 #include "llviewerregion.h"
@@ -49,86 +45,111 @@ class LLViewerStatsRecorder : public LLSingleton<LLViewerStatsRecorder>
 	~LLViewerStatsRecorder();
 
  public:
-	void objectUpdateFailure(U32 local_id, const EObjectUpdateType update_type, S32 msg_size)
-	{
-#if LL_RECORD_VIEWER_STATS
-		recordObjectUpdateFailure(local_id, update_type, msg_size);
-#endif
+	// Enable/disable stats recording.  This is broken down into two
+	// flags so we can record stats without writing them to the log
+	// file.  This is useful to analyzing updates for scene loading.
+   void enableObjectStatsRecording(bool enable, bool logging = false);
+
+	bool isEnabled() const { return mEnableStatsRecording; }
+	bool isLogging() const { return mEnableStatsLogging; }
+
+	void objectUpdateFailure(S32 msg_size)
+    {
+        if (mEnableStatsRecording)
+        {
+            recordObjectUpdateFailure(msg_size);
+        }
 	}
 
-	void cacheMissEvent(U32 local_id, const EObjectUpdateType update_type, U8 cache_miss_type, S32 msg_size)
+	void cacheMissEvent(U8 cache_miss_type)
 	{
-#if LL_RECORD_VIEWER_STATS
-		recordCacheMissEvent(local_id, update_type, cache_miss_type, msg_size);
-#endif
+        if (mEnableStatsRecording)
+        {
+            recordCacheMissEvent(cache_miss_type);
+        }
+    }
+
+	void cacheHitEvent()
+    {
+        if (mEnableStatsRecording)
+        {
+            recordCacheHitEvent();
+        }
+    }
+
+    void objectUpdateEvent(const EObjectUpdateType update_type)
+    {
+		if (mEnableStatsRecording)
+		{
+			recordObjectUpdateEvent(update_type);
+		}
 	}
 
-	void objectUpdateEvent(U32 local_id, const EObjectUpdateType update_type, LLViewerObject * objectp, S32 msg_size)
-	{
-#if LL_RECORD_VIEWER_STATS
-		recordObjectUpdateEvent(local_id, update_type, objectp, msg_size);
-#endif
-	}
-
-	void cacheFullUpdate(U32 local_id, const EObjectUpdateType update_type, LLViewerRegion::eCacheUpdateResult update_result, LLViewerObject* objectp, S32 msg_size)
-	{
-#if LL_RECORD_VIEWER_STATS
-		recordCacheFullUpdate(local_id, update_type, update_result, objectp, msg_size);
-#endif
+	void cacheFullUpdate(LLViewerRegion::eCacheUpdateResult update_result)
+    {
+		if (mEnableStatsRecording)
+		{
+			recordCacheFullUpdate(update_result);
+		}
 	}
 
 	void requestCacheMissesEvent(S32 count)
 	{
-#if LL_RECORD_VIEWER_STATS
-		recordRequestCacheMissesEvent(count);
-#endif
+        if (mEnableStatsRecording)
+        {
+            recordRequestCacheMissesEvent(count);
+        }
 	}
 
 	void textureFetch(S32 msg_size)
 	{
-#if LL_RECORD_VIEWER_STATS
-		recordTextureFetch(msg_size);
-#endif
+        if (mEnableStatsRecording)
+        {
+            recordTextureFetch(msg_size);
+        }
 	}
 
-	void log(F32 interval)
+	void idle()
 	{
-#if LL_RECORD_VIEWER_STATS
-		writeToLog(interval);
-#endif
+        writeToLog(mInterval);
 	}
 
 	F32 getTimeSinceStart();
 
 private:
-	void recordObjectUpdateFailure(U32 local_id, const EObjectUpdateType update_type, S32 msg_size);
-	void recordCacheMissEvent(U32 local_id, const EObjectUpdateType update_type, U8 cache_miss_type, S32 msg_size);
-	void recordObjectUpdateEvent(U32 local_id, const EObjectUpdateType update_type, LLViewerObject * objectp, S32 msg_size);
-	void recordCacheFullUpdate(U32 local_id, const EObjectUpdateType update_type, LLViewerRegion::eCacheUpdateResult update_result, LLViewerObject* objectp, S32 msg_size);
+	void recordObjectUpdateFailure(S32 msg_size);
+	void recordCacheMissEvent(U8 cache_miss_type);
+	void recordCacheHitEvent();
+	void recordObjectUpdateEvent(const EObjectUpdateType update_type);
+	void recordCacheFullUpdate(LLViewerRegion::eCacheUpdateResult update_result);
 	void recordRequestCacheMissesEvent(S32 count);
 	void recordTextureFetch(S32 msg_size);
 	void writeToLog(F32 interval);
+    void closeStatsFile();
+	void makeStatsFileName();
 
 	static LLViewerStatsRecorder* sInstance;
 
-	LLFILE *	mObjectCacheFile;		// File to write data into
+	LLFILE *	mStatsFile;			// File to write data into
+    std::string mStatsFileName;
+
 	LLFrameTimer	mTimer;
-	F64			mStartTime;
+	F64			mFileOpenTime;
 	F64			mLastSnapshotTime;
+    F32         mInterval;					// Interval between data log writes
+    F32         mMaxDuration;				// Time limit on file
+
+	bool        mEnableStatsRecording;		// Set to true to enable recording stats data
+    bool		mEnableStatsLogging;		// Set true to write stats to log file
+	bool		mSkipSaveIfZeros;			// Set true to skip saving stats if all values are zero	
 
 	S32			mObjectCacheHitCount;
-	S32			mObjectCacheHitSize;
 	S32			mObjectCacheMissFullCount;
-	S32			mObjectCacheMissFullSize;
 	S32			mObjectCacheMissCrcCount;
-	S32			mObjectCacheMissCrcSize;
 	S32			mObjectFullUpdates;
-	S32			mObjectFullUpdatesSize;
 	S32			mObjectTerseUpdates;
-	S32			mObjectTerseUpdatesSize;
 	S32			mObjectCacheMissRequests;
 	S32			mObjectCacheMissResponses;
-	S32			mObjectCacheMissResponsesSize;
 	S32			mObjectCacheUpdateDupes;
 	S32			mObjectCacheUpdateChanges;
 	S32			mObjectCacheUpdateAdds;
@@ -137,9 +158,7 @@ private:
 	S32			mObjectUpdateFailuresSize;
 	S32			mTextureFetchSize;
 
-
 	void	clearStats();
 };
 
 #endif // LLVIEWERSTATSRECORDER_H
-
diff --git a/indra/newview/skins/default/xui/en/menu_viewer.xml b/indra/newview/skins/default/xui/en/menu_viewer.xml
index f72d1f0bf9..d78ebf17e3 100644
--- a/indra/newview/skins/default/xui/en/menu_viewer.xml
+++ b/indra/newview/skins/default/xui/en/menu_viewer.xml
@@ -3463,14 +3463,24 @@ function="World.EnvPreset"
                 <menu_item_call.on_click
                  function="Advanced.DumpRegionObjectCache" />
             </menu_item_call>
-        
-<menu_item_call
-             label="Interest List: Full Update"
-             name="Interest List: Full Update"
+
+			<menu_item_check
+			  label="Interest List 360 Mode"
+             name="Interest List: 360 Mode"
              shortcut="alt|shift|I">
-                <menu_item_call.on_click
-                 function="Advanced.InterestListFullUpdate" />
-            </menu_item_call>
+			   <menu_item_check.on_check
+				function="Advanced.CheckInterestList360Mode" />
+			   <menu_item_check.on_click
+				  function="Advanced.ToggleInterestList360Mode" />
+            </menu_item_check>
+			<menu_item_check
+             label="Record Stats to File"
+             name="Stats Recorder File">
+				<menu_item_check.on_check
+                 function="Advanced.CheckStatsRecorder" />
+				<menu_item_check.on_click
+                 function="Advanced.ToggleStatsRecorder" />
+			</menu_item_check>
         </menu>
         <menu
          create_jump_keys="true"
-- 
cgit v1.2.3