Skip to content

Commit 1fea761

Browse files
perf: prune conflict classification work (#419) (#420)
Co-authored-by: Jonathan Weinberg <2151251+jonathanweinberg@users.noreply.github.com>
1 parent 61762be commit 1fea761

2 files changed

Lines changed: 154 additions & 32 deletions

File tree

src/ShareSurfer/Private/Get-ShareSurferConflicts.ps1

Lines changed: 102 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ function Get-ShareSurferConflicts {
1616
$conflicts = New-Object System.Collections.ArrayList
1717
$sharePermissionsList = @(ConvertTo-ShareSurferArray $SharePermissions)
1818
$aclEntriesList = @(ConvertTo-ShareSurferArray $AclEntries)
19+
$aclTotal = $aclEntriesList.Count
20+
$conflictStatusIntervalSeconds = Get-ShareSurferConflictStatusIntervalSeconds -RequestedSeconds $StatusIntervalSeconds -AclRowCount $aclTotal
1921
$statusClock = [System.Diagnostics.Stopwatch]::StartNew()
2022
$statusState = @{ LastSeconds = -999.0 }
2123
$showConflictProgress = [bool]$ShowProgress -and -not [bool]$Quiet
@@ -32,7 +34,7 @@ function Get-ShareSurferConflicts {
3234
}
3335

3436
$elapsed = [double]$statusClock.Elapsed.TotalSeconds
35-
if ($Force -or $StatusIntervalSeconds -le 0 -or ($elapsed - $statusState.LastSeconds) -ge $StatusIntervalSeconds) {
37+
if ($Force -or $conflictStatusIntervalSeconds -le 0 -or ($elapsed - $statusState.LastSeconds) -ge $conflictStatusIntervalSeconds) {
3638
$statusState.LastSeconds = $elapsed
3739
Write-ShareSurferStatus -Phase 'Export' -Message $Message
3840
}
@@ -70,43 +72,63 @@ function Get-ShareSurferConflicts {
7072
$ntfsAllowPatterns = @{}
7173
$ntfsDenyItemIdentityKeys = @{}
7274
$aclIndex = 0
73-
$aclTotal = $aclEntriesList.Count
7475
foreach ($ace in $aclEntriesList) {
7576
$aclIndex++
76-
if (-not $ntfsByShare.ContainsKey($ace.ShareId)) {
77-
$ntfsByShare[$ace.ShareId] = @{}
77+
$shareId = [string]$ace.ShareId
78+
if (-not $ntfsByShare.ContainsKey($shareId)) {
79+
$ntfsByShare[$shareId] = @{}
7880
}
7981

8082
$identityKey = ([string]$ace.Identity).ToUpperInvariant()
81-
$ntfsByShare[$ace.ShareId][$identityKey] = $true
82-
83-
$shareIdentityKey = '{0}|{1}' -f [string]$ace.ShareId, $identityKey
84-
if (-not $ntfsIdentityExamples.ContainsKey($shareIdentityKey)) {
85-
$ntfsIdentityExamples[$shareIdentityKey] = @{
86-
ShareId = [string]$ace.ShareId
87-
IdentityKey = $identityKey
88-
Ace = $ace
89-
EvidenceState = New-ShareSurferConflictEvidenceState
90-
}
83+
$ntfsByShare[$shareId][$identityKey] = $true
84+
85+
$shareMap = @{}
86+
if ($sharePermissionsByShare.ContainsKey($shareId)) {
87+
$shareMap = $sharePermissionsByShare[$shareId]
9188
}
92-
Add-ShareSurferConflictEvidence -State $ntfsIdentityExamples[$shareIdentityKey]['EvidenceState'] -Ace $ace
89+
$shareMapHasIdentity = $shareMap.ContainsKey($identityKey)
90+
$hasBroadAllowGate = ($shareHasBroadAllowGate.ContainsKey($shareId) -and [bool]$shareHasBroadAllowGate[$shareId])
9391

94-
$accessType = Get-ShareSurferAccessType $ace.AccessControlType
95-
if ($accessType -eq 'Allow') {
96-
$allowPatternKey = '{0}|{1}|{2}' -f [string]$ace.ShareId, $identityKey, ([string]$ace.Rights).ToUpperInvariant()
97-
if (-not $ntfsAllowPatterns.ContainsKey($allowPatternKey)) {
98-
$ntfsAllowPatterns[$allowPatternKey] = @{
99-
ShareId = [string]$ace.ShareId
92+
if ($shareMap.Count -gt 0 -and -not $shareMapHasIdentity -and -not $hasBroadAllowGate) {
93+
$shareIdentityKey = '{0}|{1}' -f $shareId, $identityKey
94+
if (-not $ntfsIdentityExamples.ContainsKey($shareIdentityKey)) {
95+
$ntfsIdentityExamples[$shareIdentityKey] = @{
96+
ShareId = $shareId
10097
IdentityKey = $identityKey
10198
Ace = $ace
102-
NtfsRank = Get-ShareSurferRightsRank -Rights $ace.Rights
10399
EvidenceState = New-ShareSurferConflictEvidenceState
104100
}
105101
}
106-
Add-ShareSurferConflictEvidence -State $ntfsAllowPatterns[$allowPatternKey]['EvidenceState'] -Ace $ace
102+
Add-ShareSurferConflictEvidence -State $ntfsIdentityExamples[$shareIdentityKey]['EvidenceState'] -Ace $ace
103+
}
104+
105+
$accessType = Get-ShareSurferAccessType $ace.AccessControlType
106+
if ($accessType -eq 'Allow') {
107+
if ($shareMapHasIdentity) {
108+
$shareIdentityCacheKey = '{0}|{1}' -f $shareId, $identityKey
109+
$shareAllowRank = 0
110+
if ($shareAllowRankByShareIdentity.ContainsKey($shareIdentityCacheKey)) {
111+
$shareAllowRank = [int]$shareAllowRankByShareIdentity[$shareIdentityCacheKey]
112+
}
113+
114+
$ntfsRank = Get-ShareSurferRightsRank -Rights $ace.Rights
115+
if ($shareAllowRank -gt 0 -and $ntfsRank -gt $shareAllowRank) {
116+
$allowPatternKey = '{0}|{1}|{2}' -f $shareId, $identityKey, ([string]$ace.Rights).ToUpperInvariant()
117+
if (-not $ntfsAllowPatterns.ContainsKey($allowPatternKey)) {
118+
$ntfsAllowPatterns[$allowPatternKey] = @{
119+
ShareId = $shareId
120+
IdentityKey = $identityKey
121+
Ace = $ace
122+
NtfsRank = $ntfsRank
123+
EvidenceState = New-ShareSurferConflictEvidenceState
124+
}
125+
}
126+
Add-ShareSurferConflictEvidence -State $ntfsAllowPatterns[$allowPatternKey]['EvidenceState'] -Ace $ace
127+
}
128+
}
107129
}
108130
elseif ($accessType -eq 'Deny') {
109-
$denyStateKey = '{0}|{1}|{2}' -f [string]$ace.ShareId, [string]$ace.ItemId, $identityKey
131+
$denyStateKey = '{0}|{1}|{2}' -f $shareId, [string]$ace.ItemId, $identityKey
110132
$ntfsDenyItemIdentityKeys[$denyStateKey] = $true
111133
}
112134

@@ -282,6 +304,7 @@ function New-ShareSurferConflictEvidenceState {
282304
ExamplePath = ''
283305
AffectedPathPrefix = ''
284306
FirstSeenPath = ''
307+
PrefixSampleCount = 0
285308
MaxDepth = 0
286309
}
287310
}
@@ -300,28 +323,53 @@ function Add-ShareSurferConflictEvidence {
300323

301324
$State['RowCount'] = [int]$State['RowCount'] + 1
302325

303-
$itemId = Get-ShareSurferConflictRowValue -Row $Ace -Name 'ItemId'
326+
$itemId = ''
327+
if ($null -ne $Ace.PSObject.Properties['ItemId']) {
328+
$itemId = [string]$Ace.PSObject.Properties['ItemId'].Value
329+
}
304330
if (-not [string]::IsNullOrWhiteSpace($itemId)) {
305331
$State['ItemIds'][$itemId] = $true
306332
}
307333

308-
$path = Get-ShareSurferConflictEvidencePath -Row $Ace
334+
$path = ''
335+
foreach ($name in @('FullPath', 'ExamplePath', 'UNCPath', 'LocalPath', 'RelativePath')) {
336+
if ($null -ne $Ace.PSObject.Properties[$name]) {
337+
$value = [string]$Ace.PSObject.Properties[$name].Value
338+
if (-not [string]::IsNullOrWhiteSpace($value)) {
339+
$path = $value
340+
break
341+
}
342+
}
343+
}
344+
309345
if (-not [string]::IsNullOrWhiteSpace($path)) {
310346
if ([string]::IsNullOrWhiteSpace([string]$State['FirstSeenPath'])) {
311347
$State['FirstSeenPath'] = $path
312348
}
313349
if ([string]::IsNullOrWhiteSpace([string]$State['ExamplePath'])) {
314350
$State['ExamplePath'] = $path
315351
}
316-
if ([string]::IsNullOrWhiteSpace([string]$State['AffectedPathPrefix'])) {
317-
$State['AffectedPathPrefix'] = $path
318-
}
319-
else {
320-
$State['AffectedPathPrefix'] = Get-ShareSurferCommonPathPrefix -Left ([string]$State['AffectedPathPrefix']) -Right $path
352+
353+
$prefixSampleCount = [int]$State['PrefixSampleCount']
354+
if ($prefixSampleCount -lt 64 -or [string]::IsNullOrWhiteSpace([string]$State['AffectedPathPrefix'])) {
355+
if ([string]::IsNullOrWhiteSpace([string]$State['AffectedPathPrefix'])) {
356+
$State['AffectedPathPrefix'] = $path
357+
}
358+
else {
359+
$State['AffectedPathPrefix'] = Get-ShareSurferCommonPathPrefix -Left ([string]$State['AffectedPathPrefix']) -Right $path
360+
}
361+
$State['PrefixSampleCount'] = $prefixSampleCount + 1
321362
}
322363
}
323364

324-
$depth = Get-ShareSurferConflictEvidenceDepth -Row $Ace
365+
$depth = 0
366+
if ($null -ne $Ace.PSObject.Properties['Depth']) {
367+
$depthText = [string]$Ace.PSObject.Properties['Depth'].Value
368+
[void][int]::TryParse($depthText, [ref]$depth)
369+
}
370+
if ($depth -le 0 -and -not [string]::IsNullOrWhiteSpace($path)) {
371+
$depth = @($path -split '[\\/]' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }).Count
372+
}
325373
if ($depth -gt [int]$State['MaxDepth']) {
326374
$State['MaxDepth'] = $depth
327375
}
@@ -415,6 +463,28 @@ function Get-ShareSurferConflictEvidenceDepth {
415463
@($path -split '[\\/]' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }).Count
416464
}
417465

466+
function Get-ShareSurferConflictStatusIntervalSeconds {
467+
param(
468+
[int] $RequestedSeconds = 15,
469+
470+
[int] $AclRowCount = 0
471+
)
472+
473+
if ($RequestedSeconds -le 0) {
474+
return $RequestedSeconds
475+
}
476+
477+
if ($AclRowCount -ge 100000 -and $RequestedSeconds -lt 60) {
478+
return 60
479+
}
480+
481+
if ($AclRowCount -ge 25000 -and $RequestedSeconds -lt 30) {
482+
return 30
483+
}
484+
485+
$RequestedSeconds
486+
}
487+
418488
function Get-ShareSurferCommonPathPrefix {
419489
param(
420490
[string] $Left = '',

tests/Invoke-ShareSurferTests.ps1

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1630,6 +1630,58 @@ $tests = @(
16301630
Assert-Equal $restriction.EvidenceCompleteness 'RolledUp' 'Rolled-up restrictions should be labeled for dashboard review.'
16311631
}
16321632
},
1633+
@{
1634+
Name = 'Get-ShareSurferConflicts prunes share-covered nonrestrictive ACL evidence at scale'
1635+
Body = {
1636+
Import-Module $moduleManifest -Force
1637+
$sharePermissions = @(
1638+
[pscustomobject]@{ ShareId = 'share-001'; Identity = 'CONTOSO\FinanceReaders'; Rights = 'Full'; AccessControlType = 'Allow' }
1639+
)
1640+
$aclEntries = for ($i = 0; $i -lt 50000; $i++) {
1641+
[pscustomobject]@{
1642+
ShareId = 'share-001'
1643+
ItemId = 'item-{0}' -f $i
1644+
FullPath = '\\files01\Finance\Inherited\Department\Team\Folder{0}' -f $i
1645+
Identity = 'CONTOSO\FinanceReaders'
1646+
Rights = 'ReadAndExecute'
1647+
AccessControlType = 'Allow'
1648+
Depth = 5
1649+
}
1650+
}
1651+
1652+
$shareSurferModule = Get-Module ShareSurfer
1653+
$result = $null
1654+
$elapsed = Measure-Command {
1655+
$result = @(& $shareSurferModule {
1656+
param($SharePermissions, $AclEntries)
1657+
Get-ShareSurferConflicts -SharePermissions $SharePermissions -AclEntries $AclEntries -Quiet
1658+
} $sharePermissions $aclEntries)
1659+
}
1660+
1661+
Assert-Equal @($result).Count 0 'Share-covered NTFS rows that are not broader than the share gate should not produce conflicts.'
1662+
Assert-True ($elapsed.TotalSeconds -lt 15) ('Conflict classification should prune non-conflict ACL rows before building rollup evidence. ElapsedSeconds={0:N2}' -f $elapsed.TotalSeconds)
1663+
}
1664+
},
1665+
@{
1666+
Name = 'Get-ShareSurferConflicts slows high-volume progress heartbeat without disabling forced progress'
1667+
Body = {
1668+
Import-Module $moduleManifest -Force
1669+
$shareSurferModule = Get-Module ShareSurfer
1670+
$intervals = & $shareSurferModule {
1671+
[pscustomobject]@{
1672+
SmallDefault = Get-ShareSurferConflictStatusIntervalSeconds -RequestedSeconds 15 -AclRowCount 1000
1673+
MediumDefault = Get-ShareSurferConflictStatusIntervalSeconds -RequestedSeconds 15 -AclRowCount 25000
1674+
LargeDefault = Get-ShareSurferConflictStatusIntervalSeconds -RequestedSeconds 15 -AclRowCount 543220
1675+
Forced = Get-ShareSurferConflictStatusIntervalSeconds -RequestedSeconds 0 -AclRowCount 543220
1676+
}
1677+
}
1678+
1679+
Assert-Equal ([int]$intervals.SmallDefault) 15 'Small conflict runs should keep the requested heartbeat interval.'
1680+
Assert-Equal ([int]$intervals.MediumDefault) 30 'Medium conflict runs should avoid overly chatty progress lines.'
1681+
Assert-Equal ([int]$intervals.LargeDefault) 60 'Large conflict runs should avoid every-15-second scrollback during long ACL indexing.'
1682+
Assert-Equal ([int]$intervals.Forced) 0 'A forced zero-second interval should remain available for deterministic tests and explicit diagnostics.'
1683+
}
1684+
},
16331685
@{
16341686
Name = 'Get-ShareSurferConflicts avoids false merges across identities shares rights and direct deny items'
16351687
Body = {

0 commit comments

Comments
 (0)