200-кратное снижение производительности связано с операциями DataTable. Другие операции все еще медленнее в QTP, чем в MMDRV, но не с таким ужасным фактором.
Я обошел это, «кэшируя» все вызовы DataTable в пользовательской структуре (на самом деле это коллекция объектов). Построение этого занимает 5 секунд, так как я запрашиваю много листов и свойств параметров. Обработка моей структуры вместо вызова свойств DTSheet и DTParameter выполняется намного быстрее и достаточно быстро.
Я подозреваю, что в QTP все обращения к таблицам данных осуществляются через пользовательский элемент управления Excel, который они (HP) лицензировали от третьей стороны, в то время как в MMDRV они используют код, который более плотно интегрирован, что приводит к снижению затрат на вызов.
Герцог сказал бы: "Хахаха, что за беспорядок"
** Обновление ** По запросу, вот схема того, что я подразумеваю под «кэшированием» вызовов DataTable. Это какой-то код, так что будьте готовы ...
Вот этот беспорядок (извините, у меня нет времени переводить встроенные комментарии на немецком языке прямо сейчас) (и извините за форматирование, я, очевидно, ничего не могу с этим поделать, может быть, вы хотите сократить и вставьте это в редактор QTP):
Все начинается с общего класса Container, в котором я могу хранить (и получать доступ по индексу) N ссылок на объекты:
' Container-Klasse, die N Objektreferenzen aufnehmen kann.
Class TContainer
Public iItems() ' Array, das die Objektreferenzen aufnimmt
Private iItemsHaveUBound ' True, wenn das Array mindestens ein Element hat
' Konstruktor
Private Sub Class_Initialize
iItemsHaveUBound=false ' Kein Element in iItems vorhanden
End Sub
' Anzahl der enthaltenen Objektreferenzen?
Public Property Get Count
If iItemsHaveUBound Then ' Nur wenn > 0 Elemente enthalten sind (also mindestens einmal ReDim Preserve für iItems gelaufen ist),
' können wir UBound aufrufen. Macht keinen Sinn, ist aber so, ein UBound (E) liefert für ein frisches Private E() einen Subscript error...
Count=UBound (iItems)+1 ' Grösstmöglicher Index+1, da Zählung bei 0 beginnt, und 0-basierender Index+1 = Abzahl
else
Count=0 ' Jungfräuliches iItems(), direkt 0 liefern
End If
End Property
' Getter für indizierte Referenz (Index ist 1-basierend!)
Public Default Property Get Item (ByVal Index)
Set Item=iItems(Index-1)
End Property
' Setter für indizierte Zuweisung (Index ist 1-basierend!)
Public Property Set Item (ByVal Index, ByVal Val)
' MBLogDebugComment "SetItem","Index=" & Index
If Count <= (Index-1) Then
ReDim Preserve iItems (Index-1)
iItemsHaveUBound=true
End If
Set iItems(Index-1)=Val
End Property
Public Property Get AddItem (ByVal Val)
Item(Count+1)=Val
Set AddItem=Val
End Property
End Class
Я использовал специальные имена столбцов, чтобы придать столбцам особые значения. DetectColumnKind
обнаруживает это значение в соответствии с именем и выплевывает "enum". Вот оно (я не покажу DetectColumnKind
здесь):
' Von MBCollectAllTestData unterstützte Spaltenarten in Datentabellen
Private Const ckData = 0
Private Const ckReference = 1
Private Const ckComment = 2
Теперь приходит реальный материал:
Контейнер, содержащий N листовых представлений. Я собираю свойства каждого листа и сохраняю их в этом контейнере.
' Klassen, die die Tabellenbkattstrukturen repräsentieren. Hintergrund ist ein ganz abgefahrener: Der Kollektor muss sich die Spaltenstrukturen aller
' intensiv anschauen, um seinen Job zu machen (Verweise verstehen, Klassencode generieren, Zuweisungscode generieren). Dafür greift er wieder und wieder
' auf DTSheet- und DTParameter-Instanzen zu. Das ist performancemässig aber sehr, sehr teuer (warum auch immer!). Um erträgliche Laufzeiten zu erhalten,
' enumeriert der Kollektor im helper BuildTestDataDescr die Sheets und deren Spalten und merkt sich in eigenen Datenstrukturen alles, was er später
' über die Spalten so wissen muss. Anschliessend macht der Kollektor seinen Job anhand dieser Repräsentationen, nicht mehr anhand der
' DataTable-Eigenschaften. Das ergibt funktional das gleiche, macht aber performancemässig einen Riesen-Unterschied.
' Klasse, die eine Tabellenblattspalte repräsentiert
Class TestDataColumnDescr
Public Column ' as DTParameter; Referenz auf die Original-Spalte
Public ColumnName ' as String; der Name der Spalte
Public ColumnKind ' fertig ausgerechnete Spaltenart in Sachen Kollektor
Public ColumnRefdSheet ' as DTSheet; bei Verweisspalte: das verwiesene Sheet
Public ColumnRefdSheetName ' as String; bei Verweisspalte: der Name des verwiesenen Sheets
Public ColumnRefdSheetDescr ' as TestDataSheetDescr; bei Verweisspalte: Referenz auf den TestDataSheetDescr-Descriptor des verwiesenen Sheets
Public ColumnRefdSheetIDColumn ' as DTParameter; bei Verweisspalte: Referenz auf die Original-ID-Spalte des verwiesenen Sheets
Public ColumnRefdSheetPosColumn ' as DTParameter; bei Verweisspalte: Referenz auf die Original-Pos-Spalte des verwiesenen Sheets (Nothing, wenn 1:1)
' Konstruktor
Private Sub Class_Initialize
End Sub
End Class
' Klasse, die ein Tabellenblatt repräsentiert
Class TestDataSheetDescr
Public Sheet ' as DTSheet; Referenz auf das Original-Sheet
Public SheetName ' as String; Name des Sheets
Public SheetRowCount ' as Integer; Anzahl Zeilen im Original-Sheet
Public SheetColumnCount ' as Integer; Anzahl Spalten im Original-Sheet
Public SheetColumn ' as TContainer; Container aller Spaltendescriptoren (TestDataColumnDescr)
' Konstruktor
Private Sub Class_Initialize
Set SheetColumn=New TContainer
End Sub
End Class
Вот материал для создания содержимого контейнера:
' Container aller Tabellenblattrepräsentationen
Dim TestDataDescr ' wird in BuildTestDataDescr instanziiert
' Aufbau von Tabellenblattrepräsentationen, damit Kollektor nicht dauernd DataSheet-Funktionen aufrufen muss. TestDataDescr instanziieren, aufbauen.
Public Sub BuildTestDataDescr
' Build N Sheet Descriptors
Dim SheetIndex
Dim ColumnIndex
Dim S
Dim S1
Dim S2
Dim Index
dim SheetDescr, ColumnDescr
' Zunächst die N Sheet-Descriptoren mit ihren Spaltendescriptoren anlegen
'Services.StartTransaction "BuildTestDataDescr"
Set TestDataDescr = New TContainer
For SheetIndex=1 to DataTable.GetSheetCount
set SheetDescr = New TestDataSheetDescr
With TestDataDescr.AddItem (SheetDescr)
Set .Sheet=DataTable.GetSheet (SheetIndex)
.SheetName=.Sheet.Name
.SheetRowCount=.Sheet.GetRowCount
.SheetColumnCount=.Sheet.GetParameterCount
Set S=.Sheet ' .Sheet ist im folgenden With nicht erreichbar, keine Ahnung, warum (nested Withes funken nicht anscheinend)
For ColumnIndex=1 to .SheetColumnCount
set ColumnDescr = New TestDataColumnDescr
With .SheetColumn.AddItem (ColumnDescr)
Set .Column=S.GetParameter (ColumnIndex)
.ColumnName=.Column.Name
End With
Next
End With
Next
' Jetzt etwaige Verweisspalten mit zugehöriger Info anreichern (wir machen das in einem zweiten Schritt, damit wir garantiert zu allen
' verwiesenen Blättern einen Descriptor finden -- ohne Rekursion und komplizierten Abbruchbedingungen bei zyklischen Verweisen...); ferner
' müssen die Namen von auswahltabellenbasierten Spalten angepasst werden:
For SheetIndex=1 to TestDataDescr.Count
With TestDataDescr(SheetIndex)
For ColumnIndex=1 to .SheetColumnCount
Set S=.Sheet ' .Sheet ist im folgenden With nicht erreichbar, keine Ahnung, warum (nested Withes funken nicht anscheinend)
With .SheetColumn(ColumnIndex)
.ColumnKind=DetectColumnKind (.ColumnName,S1,S2)
Select Case .ColumnKind
Case ckComment
' Nuttin', weil: Ist ja eine Gruppier- oder Kommentarspalte -- ignorieren
Case ckData
' Datenspalte -- hier nichts weiter zu tun
.ColumnName=S1 ' ausser: Namen bereinigen (hat nur Folgen für auswahllistenbasierte Spalten)
Case ckReference
' Verweisspalte -- merken, was später immer wieder an info benötigt wird
.ColumnName=S1
Set .ColumnRefdSheet=MBFindSheet (S2)
If .ColumnRefdSheet is Nothing Then
MBErrorAbort "MBUtil.MBCollectAllTestData", _
"Fehler beim Definieren von Klassen;" & vbNewline _
& "Spalte '" & .ColumnName & "' definiert einen Verweis auf Datentabellenblatt '" & S2 & "', welches nicht existiert." & vbNewline _
& "Bitte überprüfen Sie die entsprechenden Datentabellenblätter"
End If
.ColumnRefdSheetName=.ColumnRefdSheet.Name
Set .ColumnRefdSheetIDColumn=.ColumnRefdSheet.GetParameter ("ID")
Set .ColumnRefdSheetPosColumn=MBFindColumn (.ColumnRefdSheet,"Pos")
For Index=1 to TestDataDescr.Count
If TestDataDescr(Index).SheetName = .ColumnRefdSheetName then
Exit For
End If
Next
Set .ColumnRefdSheetDescr=TestDataDescr(Index)
End Select
End With
Next
End With
Next
'Services.EndTransaction "BuildTestDataDescr"
End Sub
Основываясь на информации в контейнере, я использую структуры в TestDataDescr для перебора столбцов и т. Д.
Как в этом примере, который получает один элемент контейнера (SourceSheetDescr), просматривает каждый столбец и «что-то» делает в соответствии с типом столбца, который является частью информации, встроенной в элемент контейнера:
For ParamIndex=1 to SourceSheetDescr.SheetColumnCount
With SourceSheetDescr.SheetColumn(ParamIndex)
Select Case .ColumnKind
Case ckComment
' Do something
Case ckData
' Do something else Case ckReference
' Do other stuff End Select
End With
Next
Таким образом, я избегаю запроса DTSheet.GetParameter () и не вызываю любые другие методы DataTable.
Это, конечно, только один из примеров использования информации, содержащейся в контейнере.
В наших типичных случаях использования это утроило производительность по сравнению с вызовом методов DataTable , и даже при том, что мы уже избежали всех избыточных вызовов и сделали все другие очевидные оптимизации в традиционном коде доступа к таблице данных.