From 49178eb1e56abef47307aebe9c0a6b5161622dce Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Sun, 13 Sep 2009 14:31:16 +0000 Subject: [PATCH] Worked on UsageDataCollector. git-svn-id: svn://svn.sharpdevelop.net/sharpdevelop/trunk@4919 1ccf3a8d-04fe-1044-b7c0-cef0b8235c61 --- .../UsageDataCollector/AnalyticsMonitor.cs | 38 +++- .../UsageDataCollector/CollectedDataView.xaml | 21 ++ .../CollectedDataView.xaml.cs | 41 ++++ .../Misc/UsageDataCollector/OptionPage.xaml | 3 + .../UsageDataCollector/OptionPage.xaml.cs | 23 ++ .../UsageDataCollector.csproj | 20 +- .../UsageDataCollector/UsageDataMessage.cs | 93 ++++++++ ...ionWriter.cs => UsageDataSessionWriter.cs} | 139 +++++++----- .../UsageDataCollector/UsageDataUploader.cs | 208 ++++++++++++++++++ 9 files changed, 530 insertions(+), 56 deletions(-) create mode 100644 src/AddIns/Misc/UsageDataCollector/CollectedDataView.xaml create mode 100644 src/AddIns/Misc/UsageDataCollector/CollectedDataView.xaml.cs create mode 100644 src/AddIns/Misc/UsageDataCollector/UsageDataMessage.cs rename src/AddIns/Misc/UsageDataCollector/{AnalyticsSessionWriter.cs => UsageDataSessionWriter.cs} (51%) create mode 100644 src/AddIns/Misc/UsageDataCollector/UsageDataUploader.cs diff --git a/src/AddIns/Misc/UsageDataCollector/AnalyticsMonitor.cs b/src/AddIns/Misc/UsageDataCollector/AnalyticsMonitor.cs index 24ff5460e0..a885f5d8ca 100644 --- a/src/AddIns/Misc/UsageDataCollector/AnalyticsMonitor.cs +++ b/src/AddIns/Misc/UsageDataCollector/AnalyticsMonitor.cs @@ -6,10 +6,11 @@ // using System; +using System.IO; using ICSharpCode.Core; using ICSharpCode.Core.Services; using ICSharpCode.SharpDevelop; -using System.IO; +using System.Threading; namespace ICSharpCode.UsageDataCollector { @@ -56,15 +57,40 @@ namespace ICSharpCode.UsageDataCollector public void OpenSession() { + bool sessionOpened = false; lock (lockObj) { if (session == null) { try { session = new AnalyticsSessionWriter(dbFileName); - } catch (DatabaseTooNewException) { - LoggingService.Warn("Could not use AnalyticsMonitor: too new version of database"); + session.AddEnvironmentData("appVersion", RevisionClass.FullVersion); + session.AddEnvironmentData("language", ResourceService.Language); + sessionOpened = true; + } catch (IncompatibleDatabaseException ex) { + if (ex.ActualVersion < ex.ExpectedVersion) { + LoggingService.Info("AnalyticsMonitor: " + ex.Message + ", removing old database"); + // upgrade database by deleting the old one + TryDeleteDatabase(); + try { + session = new AnalyticsSessionWriter(dbFileName); + } catch (IncompatibleDatabaseException ex2) { + LoggingService.Warn("AnalyticsMonitor: Could upgrade database: " + ex2.Message); + } + } else { + LoggingService.Warn("AnalyticsMonitor: " + ex.Message); + } } } } + if (sessionOpened) { + UsageDataUploader uploader = new UsageDataUploader(dbFileName); + ThreadPool.QueueUserWorkItem(delegate { uploader.StartUpload(); }); + } + } + + public string GetTextForStoredData() + { + UsageDataUploader uploader = new UsageDataUploader(dbFileName); + return uploader.GetTextForStoredData(); } void TryDeleteDatabase() @@ -73,8 +99,10 @@ namespace ICSharpCode.UsageDataCollector CloseSession(); try { File.Delete(dbFileName); - } catch (IOException) { - } catch (AccessViolationException) { + } catch (IOException ex) { + LoggingService.Warn("AnalyticsMonitor: Could delete database: " + ex.Message); + } catch (AccessViolationException ex) { + LoggingService.Warn("AnalyticsMonitor: Could delete database: " + ex.Message); } } } diff --git a/src/AddIns/Misc/UsageDataCollector/CollectedDataView.xaml b/src/AddIns/Misc/UsageDataCollector/CollectedDataView.xaml new file mode 100644 index 0000000000..82e95d603f --- /dev/null +++ b/src/AddIns/Misc/UsageDataCollector/CollectedDataView.xaml @@ -0,0 +1,21 @@ + + + + This window shows the data that was collected but not yet uploaded. + Privacy Statement diff --git a/src/AddIns/Misc/UsageDataCollector/OptionPage.xaml.cs b/src/AddIns/Misc/UsageDataCollector/OptionPage.xaml.cs index af1d3a8950..ae45e52bef 100644 --- a/src/AddIns/Misc/UsageDataCollector/OptionPage.xaml.cs +++ b/src/AddIns/Misc/UsageDataCollector/OptionPage.xaml.cs @@ -6,6 +6,11 @@ // using System; +using System.Diagnostics; +using System.Windows; +using System.Windows.Documents; +using System.Windows.Navigation; + using ICSharpCode.SharpDevelop.Gui; namespace ICSharpCode.UsageDataCollector @@ -18,6 +23,7 @@ namespace ICSharpCode.UsageDataCollector public OptionPage() { InitializeComponent(); + AddHandler(Hyperlink.RequestNavigateEvent, new RequestNavigateEventHandler(OnRequestNavigate)); } public override void LoadOptions() @@ -29,6 +35,7 @@ namespace ICSharpCode.UsageDataCollector else declineRadio.IsChecked = true; } + showCollectedDataButton.IsEnabled = acceptRadio.IsChecked ?? false; } public override bool SaveOptions() @@ -39,5 +46,21 @@ namespace ICSharpCode.UsageDataCollector AnalyticsMonitor.Enabled = false; return base.SaveOptions(); } + + void OnRequestNavigate(object sender, RequestNavigateEventArgs e) + { + e.Handled = true; + try { + Process.Start(e.Uri.ToString()); + } catch { + // catch exceptions - e.g. incorrectly installed web browser + } + } + + void ShowCollectedDataButton_Click(object sender, RoutedEventArgs e) + { + string data = AnalyticsMonitor.Instance.GetTextForStoredData(); + (new CollectedDataView(data) { Owner = Window.GetWindow(this), ShowInTaskbar = false }).ShowDialog(); + } } } \ No newline at end of file diff --git a/src/AddIns/Misc/UsageDataCollector/UsageDataCollector.csproj b/src/AddIns/Misc/UsageDataCollector/UsageDataCollector.csproj index 66d47ae583..21277c2567 100644 --- a/src/AddIns/Misc/UsageDataCollector/UsageDataCollector.csproj +++ b/src/AddIns/Misc/UsageDataCollector/UsageDataCollector.csproj @@ -43,12 +43,18 @@ 3.0 + + 3.5 + ..\..\..\Libraries\SQLite\System.Data.SQLite.dll True + + 3.0 + @@ -65,7 +71,12 @@ Configuration\GlobalAssemblyInfo.cs - + + CollectedDataView.xaml + Code + + + OptionPage.xaml @@ -75,12 +86,19 @@ StartPageMessage.xaml Code + + + + {6C55B776-26D4-4DB3-A6AB-87E783B2F3D1} + ICSharpCode.AvalonEdit + False + {2748AD25-9C63-4E12-877B-4DCE96FBED54} ICSharpCode.SharpDevelop diff --git a/src/AddIns/Misc/UsageDataCollector/UsageDataMessage.cs b/src/AddIns/Misc/UsageDataCollector/UsageDataMessage.cs new file mode 100644 index 0000000000..a639de2c40 --- /dev/null +++ b/src/AddIns/Misc/UsageDataCollector/UsageDataMessage.cs @@ -0,0 +1,93 @@ +// +// +// +// +// $Revision$ +// + +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace ICSharpCode.UsageDataCollector +{ + [DataContract] + sealed class UsageDataMessage + { + [DataMember] + public Guid UserID; + + [DataMember] + public List Sessions = new List(); + + public UsageDataSession FindSession(long sessionID) + { + foreach (UsageDataSession s in Sessions) { + if (s.SessionID == sessionID) + return s; + } + throw new ArgumentException("Session not found."); + } + } + + [DataContract] + sealed class UsageDataSession + { + [DataMember] + public long SessionID; + + [DataMember] + public DateTime StartTime; + + [DataMember] + public DateTime? EndTime; + + [DataMember] + public List EnvironmentProperties = new List(); + + [DataMember] + public List FeatureUses = new List(); + + [DataMember] + public List Exceptions = new List(); + } + + [DataContract] + sealed class UsageDataEnvironmentProperty + { + [DataMember] + public string Name; + + [DataMember] + public string Value; + } + + [DataContract] + sealed class UsageDataFeatureUse + { + [DataMember] + public DateTime Time; + + [DataMember] + public DateTime? EndTime; + + [DataMember] + public string FeatureName; + + [DataMember] + public string ActivationMethod; + } + + [DataContract] + sealed class UsageDataException + { + [DataMember] + public DateTime Time; + + [DataMember] + public string ExceptionType; + + [DataMember] + public string StackTrace; + } +} diff --git a/src/AddIns/Misc/UsageDataCollector/AnalyticsSessionWriter.cs b/src/AddIns/Misc/UsageDataCollector/UsageDataSessionWriter.cs similarity index 51% rename from src/AddIns/Misc/UsageDataCollector/AnalyticsSessionWriter.cs rename to src/AddIns/Misc/UsageDataCollector/UsageDataSessionWriter.cs index 3e93cd1e8d..1a9fd45d94 100644 --- a/src/AddIns/Misc/UsageDataCollector/AnalyticsSessionWriter.cs +++ b/src/AddIns/Misc/UsageDataCollector/UsageDataSessionWriter.cs @@ -5,7 +5,6 @@ // $Revision$ // -using ICSharpCode.Core; using System; using System.Data.SQLite; using System.Runtime.Serialization; @@ -20,6 +19,10 @@ namespace ICSharpCode.UsageDataCollector SQLiteConnection connection; long sessionID; + /// + /// Opens/Creates the database and starts writing a new session to it. + /// + /// public AnalyticsSessionWriter(string databaseFileName) { SQLiteConnectionStringBuilder conn = new SQLiteConnectionStringBuilder(); @@ -27,11 +30,19 @@ namespace ICSharpCode.UsageDataCollector connection = new SQLiteConnection(conn.ConnectionString); connection.Open(); - InitializeTables(); - - StartSession(); + try { + InitializeTables(); + + StartSession(); + } catch { + connection.Close(); + throw; + } } + + static readonly Version expectedDBVersion = new Version(1, 0, 1); + /// /// Creates or upgrades the database /// @@ -44,8 +55,7 @@ namespace ICSharpCode.UsageDataCollector name TEXT NOT NULL PRIMARY KEY, value TEXT NOT NULL ); - INSERT OR IGNORE INTO Properties (name, value) VALUES ('dbVersion', '1.0'); - INSERT OR IGNORE INTO Properties (name, value) VALUES ('userID', '" + Guid.NewGuid().ToString() + @"'); + INSERT OR IGNORE INTO Properties (name, value) VALUES ('dbVersion', '" + expectedDBVersion.ToString() + @"'); "; cmd.ExecuteNonQuery(); } @@ -54,24 +64,27 @@ namespace ICSharpCode.UsageDataCollector string version = (string)cmd.ExecuteScalar(); if (version == null) throw new InvalidOperationException("Error retrieving database version"); - if (version != "1.0") { - throw new DatabaseTooNewException(); + Version actualDBVersion = new Version(version); + if (actualDBVersion != expectedDBVersion) { + throw new IncompatibleDatabaseException(expectedDBVersion, actualDBVersion); } } using (SQLiteCommand cmd = this.connection.CreateCommand()) { cmd.CommandText = @" + INSERT OR IGNORE INTO Properties (name, value) VALUES ('userID', '" + Guid.NewGuid().ToString() + @"'); + CREATE TABLE IF NOT EXISTS Sessions ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, startTime TEXT NOT NULL, - endTime TEXT, - appVersion TEXT, - platform TEXT, - osVersion TEXT, - processorCount INTEGER, - dotnetRuntime TEXT, - language TEXT + endTime TEXT + ); + CREATE TABLE IF NOT EXISTS Environment ( + session INTEGER NOT NULL, + name TEXT NOT NULL, + value TEXT ); CREATE TABLE IF NOT EXISTS FeatureUses ( + id INTEGER NOT NULL PRIMARY KEY, session INTEGER NOT NULL, time TEXT NOT NULL, endTime TEXT, @@ -95,33 +108,39 @@ namespace ICSharpCode.UsageDataCollector { using (SQLiteTransaction transaction = this.connection.BeginTransaction()) { using (SQLiteCommand cmd = this.connection.CreateCommand()) { - cmd.CommandText = "INSERT INTO Sessions (startTime, appVersion, platform, osVersion, processorCount, dotnetRuntime, language)" + - " VALUES (datetime(), ?, ?, ?, ?, ?, ?);"; - cmd.Parameters.Add(new SQLiteParameter { Value = RevisionClass.FullVersion }); - cmd.Parameters.Add(new SQLiteParameter { Value = Environment.OSVersion.Platform.ToString() }); - cmd.Parameters.Add(new SQLiteParameter { Value = Environment.OSVersion.Version.ToString() }); - cmd.Parameters.Add(new SQLiteParameter { Value = Environment.ProcessorCount }); - cmd.Parameters.Add(new SQLiteParameter { Value = Environment.Version.ToString() }); - cmd.Parameters.Add(new SQLiteParameter { Value = ICSharpCode.Core.ResourceService.Language }); + cmd.CommandText = "INSERT INTO Sessions (startTime) VALUES (datetime());"; cmd.ExecuteNonQuery(); } using (SQLiteCommand cmd = this.connection.CreateCommand()) { cmd.CommandText = "SELECT last_insert_rowid();"; sessionID = (long)cmd.ExecuteScalar(); } + AddEnvironmentData("platform", Environment.OSVersion.Platform.ToString()); + AddEnvironmentData("osVersion", Environment.OSVersion.Version.ToString()); + AddEnvironmentData("processorCount", Environment.ProcessorCount.ToString()); + AddEnvironmentData("dotnetRuntime", Environment.Version.ToString()); transaction.Commit(); } } void EndSession() { - using (SQLiteTransaction transaction = this.connection.BeginTransaction()) { - using (SQLiteCommand cmd = this.connection.CreateCommand()) { - cmd.CommandText = "UPDATE Sessions SET endTime = datetime() WHERE id = ?;"; - cmd.Parameters.Add(new SQLiteParameter { Value = sessionID }); - cmd.ExecuteNonQuery(); - } - transaction.Commit(); + using (SQLiteCommand cmd = this.connection.CreateCommand()) { + cmd.CommandText = "UPDATE Sessions SET endTime = datetime() WHERE id = ?;"; + cmd.Parameters.Add(new SQLiteParameter { Value = sessionID }); + cmd.ExecuteNonQuery(); + } + } + + public void AddEnvironmentData(string name, string value) + { + using (SQLiteCommand cmd = this.connection.CreateCommand()) { + cmd.CommandText = "INSERT INTO Environment (session, name, value)" + + " VALUES (?, ?, ?);"; + cmd.Parameters.Add(new SQLiteParameter { Value = sessionID }); + cmd.Parameters.Add(new SQLiteParameter { Value = name }); + cmd.Parameters.Add(new SQLiteParameter { Value = value }); + cmd.ExecuteNonQuery(); } } @@ -148,28 +167,22 @@ namespace ICSharpCode.UsageDataCollector public void WriteEndTimeForFeature(long featureID) { - using (SQLiteTransaction transaction = this.connection.BeginTransaction()) { - using (SQLiteCommand cmd = this.connection.CreateCommand()) { - cmd.CommandText = "UPDATE FeatureUses SET endTime = datetime() WHERE ROWID = ?;"; - cmd.Parameters.Add(new SQLiteParameter { Value = featureID }); - cmd.ExecuteNonQuery(); - } - transaction.Commit(); + using (SQLiteCommand cmd = this.connection.CreateCommand()) { + cmd.CommandText = "UPDATE FeatureUses SET endTime = datetime() WHERE id = ?;"; + cmd.Parameters.Add(new SQLiteParameter { Value = featureID }); + cmd.ExecuteNonQuery(); } } public void AddException(string exceptionType, string stacktrace) { - using (SQLiteTransaction transaction = this.connection.BeginTransaction()) { - using (SQLiteCommand cmd = this.connection.CreateCommand()) { - cmd.CommandText = "INSERT INTO Exceptions (session, time, type, stackTrace)" + - " VALUES (?, datetime(), ?, ?);"; - cmd.Parameters.Add(new SQLiteParameter { Value = sessionID }); - cmd.Parameters.Add(new SQLiteParameter { Value = exceptionType }); - cmd.Parameters.Add(new SQLiteParameter { Value = stacktrace }); - cmd.ExecuteNonQuery(); - } - transaction.Commit(); + using (SQLiteCommand cmd = this.connection.CreateCommand()) { + cmd.CommandText = "INSERT INTO Exceptions (session, time, type, stackTrace)" + + " VALUES (?, datetime(), ?, ?);"; + cmd.Parameters.Add(new SQLiteParameter { Value = sessionID }); + cmd.Parameters.Add(new SQLiteParameter { Value = exceptionType }); + cmd.Parameters.Add(new SQLiteParameter { Value = stacktrace }); + cmd.ExecuteNonQuery(); } } @@ -181,9 +194,35 @@ namespace ICSharpCode.UsageDataCollector } [Serializable] - public class DatabaseTooNewException : Exception + public class IncompatibleDatabaseException : Exception { - public DatabaseTooNewException() {} - protected DatabaseTooNewException(SerializationInfo info, StreamingContext context) : base(info, context) {} + public Version ExpectedVersion { get; set; } + public Version ActualVersion { get; set; } + + public IncompatibleDatabaseException() {} + + public IncompatibleDatabaseException(Version expectedVersion, Version actualVersion) + : base("Expected DB version " + expectedVersion + " but found " + actualVersion) + { + this.ExpectedVersion = expectedVersion; + this.ActualVersion = actualVersion; + } + + protected IncompatibleDatabaseException(SerializationInfo info, StreamingContext context) : base(info, context) + { + if (info != null) { + this.ExpectedVersion = (Version)info.GetValue("ExpectedVersion", typeof(Version)); + this.ActualVersion = (Version)info.GetValue("ActualVersion", typeof(Version)); + } + } + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + if (info != null) { + info.AddValue("ExpectedVersion", this.ExpectedVersion, typeof(Version)); + info.AddValue("ActualVersion", this.ActualVersion, typeof(Version)); + } + } } } \ No newline at end of file diff --git a/src/AddIns/Misc/UsageDataCollector/UsageDataUploader.cs b/src/AddIns/Misc/UsageDataCollector/UsageDataUploader.cs new file mode 100644 index 0000000000..233c47bad6 --- /dev/null +++ b/src/AddIns/Misc/UsageDataCollector/UsageDataUploader.cs @@ -0,0 +1,208 @@ +// +// +// +// +// $Revision$ +// + +using System; +using System.Collections.Generic; +using System.Data.SQLite; +using System.Globalization; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Runtime.Serialization; +using System.Xml; + +namespace ICSharpCode.UsageDataCollector +{ + /// + /// Allows uploading collected data. + /// + public class UsageDataUploader + { + string databaseFileName; + + public UsageDataUploader(string databaseFileName) + { + this.databaseFileName = databaseFileName; + } + + SQLiteConnection OpenConnection() + { + SQLiteConnectionStringBuilder conn = new SQLiteConnectionStringBuilder(); + conn.Add("Data Source", databaseFileName); + SQLiteConnection connection = new SQLiteConnection(conn.ConnectionString); + connection.Open(); + return connection; + } + + /// + /// Starts the upload of the usage data. + /// + public void StartUpload() + { + UsageDataMessage message = GetDataToBeTransmitted(false); + DataContractSerializer serializer = new DataContractSerializer(typeof(UsageDataMessage)); + using (FileStream fs = new FileStream(Path.Combine(Path.GetTempPath(), "SharpDevelopUsageData.xml.gz"), FileMode.Create, FileAccess.Write)) { + using (GZipStream zip = new GZipStream(fs, CompressionMode.Compress)) { + serializer.WriteObject(zip, message); + } + } + } + + internal UsageDataMessage GetDataToBeTransmitted(bool fetchIncompleteSessions) + { + using (SQLiteConnection connection = OpenConnection()) { + using (SQLiteTransaction transaction = connection.BeginTransaction()) { + return FetchDataForUpload(connection, fetchIncompleteSessions); + } + } + } + + public string GetTextForStoredData() + { + UsageDataMessage message = GetDataToBeTransmitted(true); + using (StringWriter w = new StringWriter()) { + using (XmlTextWriter xmlWriter = new XmlTextWriter(w)) { + xmlWriter.Formatting = Formatting.Indented; + DataContractSerializer serializer = new DataContractSerializer(typeof(UsageDataMessage)); + serializer.WriteObject(xmlWriter, message); + } + return w.ToString(); + } + } + + #region FetchDataForUpload + Version expectedDBVersion = new Version(1, 0, 1); + + UsageDataMessage FetchDataForUpload(SQLiteConnection connection, bool fetchIncompleteSessions) + { + // Check the database version + using (SQLiteCommand cmd = connection.CreateCommand()) { + cmd.CommandText = "SELECT value FROM Properties WHERE name = 'dbVersion';"; + string version = (string)cmd.ExecuteScalar(); + if (version == null) + throw new InvalidOperationException("Error retrieving database version"); + Version actualDBVersion = new Version(version); + if (actualDBVersion != expectedDBVersion) { + throw new IncompatibleDatabaseException(expectedDBVersion, actualDBVersion); + } + } + + UsageDataMessage message = new UsageDataMessage(); + // Retrieve the User ID + using (SQLiteCommand cmd = connection.CreateCommand()) { + cmd.CommandText = "SELECT value FROM Properties WHERE name = 'userID';"; + string userID = (string)cmd.ExecuteScalar(); + message.UserID = new Guid(userID); + } + + // Retrieve the list of sessions + using (SQLiteCommand cmd = connection.CreateCommand()) { + if (fetchIncompleteSessions) { + cmd.CommandText = @"SELECT id, startTime, endTime FROM Sessions;"; + } else { + // Fetch all sessions which are either closed or inactive for more than 24 hours + cmd.CommandText = @"SELECT id, startTime, endTime FROM Sessions + WHERE (endTime IS NOT NULL) + OR (ifnull((SELECT max(time) FROM FeatureUses WHERE FeatureUses.session = Sessions.id), Sessions.startTime) + < datetime('now','-1 day'));"; + } + using (SQLiteDataReader reader = cmd.ExecuteReader()) { + while (reader.Read()) { + UsageDataSession session = new UsageDataSession(); + session.SessionID = reader.GetInt64(0); + session.StartTime = reader.GetDateTime(1); + if (!reader.IsDBNull(2)) + session.EndTime = reader.GetDateTime(2); + message.Sessions.Add(session); + } + } + } + string commaSeparatedSessionIDList = GetCommaSeparatedIDList(message.Sessions); + + StringInterner stringInterning = new StringInterner(); + // Retrieve the environment + using (SQLiteCommand cmd = connection.CreateCommand()) { + cmd.CommandText = "SELECT session, name, value FROM Environment WHERE session IN (" + commaSeparatedSessionIDList + ");"; + using (SQLiteDataReader reader = cmd.ExecuteReader()) { + while (reader.Read()) { + long sessionID = reader.GetInt64(0); + UsageDataSession session = message.FindSession(sessionID); + session.EnvironmentProperties.Add( + new UsageDataEnvironmentProperty { + Name = stringInterning.Intern(reader.GetString(1)), + Value = stringInterning.Intern(reader.GetString(2)) + }); + } + } + } + + // Retrieve the feature uses + using (SQLiteCommand cmd = connection.CreateCommand()) { + cmd.CommandText = "SELECT session, time, endTime, feature, activationMethod FROM FeatureUses WHERE session IN (" + commaSeparatedSessionIDList + ");"; + using (SQLiteDataReader reader = cmd.ExecuteReader()) { + while (reader.Read()) { + long sessionID = reader.GetInt64(0); + UsageDataSession session = message.FindSession(sessionID); + UsageDataFeatureUse featureUse = new UsageDataFeatureUse(); + featureUse.Time = reader.GetDateTime(1); + if (!reader.IsDBNull(2)) + featureUse.EndTime = reader.GetDateTime(2); + featureUse.FeatureName = stringInterning.Intern(reader.GetString(3)); + featureUse.ActivationMethod = stringInterning.Intern(reader.GetString(4)); + session.FeatureUses.Add(featureUse); + } + } + } + + // Retrieve the exceptions + using (SQLiteCommand cmd = connection.CreateCommand()) { + cmd.CommandText = "SELECT session, time, type, stackTrace FROM Exceptions WHERE session IN (" + commaSeparatedSessionIDList + ");"; + using (SQLiteDataReader reader = cmd.ExecuteReader()) { + while (reader.Read()) { + long sessionID = reader.GetInt64(0); + UsageDataSession session = message.FindSession(sessionID); + UsageDataException exception = new UsageDataException(); + exception.Time = reader.GetDateTime(1); + exception.ExceptionType = stringInterning.Intern(reader.GetString(2)); + exception.StackTrace = stringInterning.Intern(reader.GetString(3)); + session.Exceptions.Add(exception); + } + } + } + + return message; + } + #endregion + + string GetCommaSeparatedIDList(IEnumerable sessions) + { + return string.Join( + ",", + sessions.Select(s => s.SessionID.ToString(CultureInfo.InvariantCulture)).ToArray()); + } + + /// + /// Helps keep the memory usage during data preparation down (there are lots of duplicate strings, and we don't + /// want to keep them in RAM repeatedly). + /// + sealed class StringInterner + { + Dictionary cache = new Dictionary(); + + public string Intern(string input) + { + if (input != null) { + string result; + if (cache.TryGetValue(input, out result)) + return result; + cache.Add(input, input); + } + return input; + } + } + } +}