Samstag, 29. August 2009

Langsame Codestränge in der Liveumgebung identifizieren

Zur Entwicklungszeit werden zeitintesive Blöcke in der Regel relativ einfach erkannt, man will aber öfters die Performance auch im Livebetrieb testen.
Vor allem Schreib- und Lesezugriffe aufs Dateisystem oder Datenbankoperationen sind predestiniert für solche Zeitmessungen.

Folgende Klasse kann verwendet werden um die Ausführungsgeschwindigkeit eines Blocks zu messen.

Die IDisposable Schnittstelle stellt sicher, dass die Dispose Methode des Objektes explizit geschrieben, bei der Zerstörung des Objektes ausgeführt wird und dabei Resourcen freigegeben werden.

   1:  public class TimeSpanLogger:IDisposable
   2:  {
   3:      public void Dispose()
   4:      {
   5:   
   6:      }
   7:  }

Im Kostruktur wird die Kennzeichnung des Messblocks weitergereicht und als private Variable festgehalten.

   1:  public TimeSpanLogger(string tag)
   2:  {
   3:      Tag = tag;
   4:  }

Bei der Zerstörung des Objektes wird die Zeit seit der Erstellung des Objektes gemessen und ins Ausgabefenster geschrieben. Für die Ausgabe können Loggingkomponenten wie zb. die Application Blocks der Enterprise Library verwendet werden.

   1:  TimeSpan ts = DateTime.Now - StartTime;
   2:   
   3:  //Debug.WriteLine ersetzen
   4:  System.Diagnostics.Debug.WriteLine(string.Concat(DateTime.Now.ToString(),
   5:  "\t", Tag, "\t", ts.TotalSeconds.ToString()));

Hier als Ganzes:

   1:  public class TimeSpanLogger:IDisposable
   2:  {
   3:      private string Tag = string.Empty;
   4:      private DateTime StartTime = DateTime.Now;
   5:   
   6:      public TimeSpanLogger(string tag)
   7:      {
   8:          Tag = tag;
   9:      }
  10:   
  11:   
  12:      public void Dispose()
  13:      {
  14:          TimeSpan ts = DateTime.Now - StartTime;
  15:   
  16:          System.Diagnostics.Debug.WriteLine(
  17:              string.Concat(DateTime.Now.ToString(),"\t", 
  18:              Tag, "\t", ts.TotalSeconds.ToString()));
  19:      }
  20:  }

Und so wirds angewandt:

   1:  using (new TimeSpanLogger("testfunc"))
   2:  {
   3:      //Some Operations
   4:  }

Beim verlassen des Blockcontexts wird die Klasse TimeSpanLogger terminiert und die Methode Dispose implizit ausgeführt.

Freitag, 28. August 2009

Globale Methoden für Datenbankzugriffe

Es wäre doch nett wenn man sich ab und zu das Leben doch ein bisschen leichter machen könnte und nicht immer Zugriffscode neu schreiben müsste. Wie wäre es also mit einer globalen Klasse die genau das macht aber nicht unbedingt mit großen Performanceeinbüssen verbunden ist.

Also, zuerst dynamisch die Parameter der Stored Procedures auslesen.Dabei wird die ermittelte ParameterCollection im Applicationscache gespeichert und zuküftig auch von dort immer geholt.

   1:  private static void DiscoverParameters(SqlCommand command, string spName)
   2:  {
   3:       //CacheKey
   4:       string CacheKey = string.Concat("SP_", spName);
   5:   
   6:       //Check if SP is in Cache
   7:       if (System.Web.HttpContext.Current.Cache[CacheKey] == null)
   8:       {
   9:            SqlCommandBuilder.DeriveParameters(command);
  10:            SqlParameterCollection paramCollection = command.Parameters;
  11:   
  12:            //SqlParameter Array
  13:            SqlParameter[] sqlParam = new SqlParameter[paramCollection.Count];
  14:            paramCollection.CopyTo(sqlParam, 0);
  15:   
  16:            System.Web.HttpRuntime.Cache[CacheKey] = sqlParam;
  17:       }
  18:       else
  19:       {
  20:            //Read from Cache 
  21:            SqlParameter[] sqlParam = (SqlParameter[])
  22:                 CacheHelperWeb.GetApplicationCacheElement(CacheKey);
  23:   
  24:            //Add to Command 
  25:            for (int i = 0; i < sqlParam.Length; i++)
  26:            {
  27:                 SqlParameter item = (SqlParameter)
  28:                      ((ICloneable)sqlParam[i]).Clone();
  29:                 command.Parameters.Add(item);
  30:            }
  31:  }
  32:  }


Dann mit Werten befüllen. Es wird eine for Schleife verwendet da bei Collections sich etwas schneller als eine foreach Schleife verhält.

   1:  private static void FillParametersValues(SqlCommand command,
   2:    params object[] parameterValues)
   3:  {
   4:       int index = 0;
   5:       int count=command.Parameters.Count;
   6:   
   7:       for (int i=0;i<count;i++) 
   8:       {
   9:            if ((command.Parameters[i].Direction == ParameterDirection.Input)||
  10:              (command.Parameters[i].Direction == ParameterDirection.InputOutput))
  11:            {
  12:                 command.Parameters[i].Value = parameterValues[index];
  13:                 index++;
  14:            }
  15:       }
  16:  }

und die Zugriffsmethode:

   1:  public static void ExecuteNonQueryIntern(string storedProcedureName,
   2:  params object[] parameterValue)
   3:  {
   4:   
   5:       using (SqlConnection connection = new SqlConnection(CONNECTIONSTRING))
   6:       {
   7:   
   8:           using (SqlCommand command = new SqlCommand
   9:             (storedProcedureName,connection))
  10:           {
  11:                command.CommandType = CommandType.StoredProcedure;
  12:   
  13:                DiscoverParameters(command, storedProcedureName);
  14:                FillParametersValues(command, parameterValue);
  15:   
  16:   
  17:                command.ExecuteNonQuery();
  18:                command.Parameters.Clear();
  19:           }
  20:       }
  21:  }

Jetzt nur noch Errorhandling und Logging einbauen und fertig. Analog kann das ganze auch für ExecuteScalar oder ExecuteReader angewandt werden. War doch ganz einfach oder?

Hier noch ein Zugriffsbeispiel:
ExecuteNonQueryIntern("SP_UpdateData",1893,"Max","Mustermann");