Coverage Report

Created: 2025-12-08 09:28

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libreoffice/svl/source/notify/broadcast.cxx
Line
Count
Source
1
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2
/*
3
 * This file is part of the LibreOffice project.
4
 *
5
 * This Source Code Form is subject to the terms of the Mozilla Public
6
 * License, v. 2.0. If a copy of the MPL was not distributed with this
7
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8
 *
9
 * This file incorporates work covered by the following license notice:
10
 *
11
 *   Licensed to the Apache Software Foundation (ASF) under one or more
12
 *   contributor license agreements. See the NOTICE file distributed
13
 *   with this work for additional information regarding copyright
14
 *   ownership. The ASF licenses this file to you under the Apache
15
 *   License, Version 2.0 (the "License"); you may not use this file
16
 *   except in compliance with the License. You may obtain a copy of
17
 *   the License at http://www.apache.org/licenses/LICENSE-2.0 .
18
 */
19
20
#include <svl/broadcast.hxx>
21
#include <svl/listener.hxx>
22
#include <svl/hint.hxx>
23
#include <o3tl/safeint.hxx>
24
#include <cassert>
25
#include <cstdint>
26
#include <algorithm>
27
28
/**
29
 Design Notes
30
 -------------------------------
31
32
 This class is extremely heavily used - we can have millions of broadcasters and listeners and we can also
33
 have a broadcaster that has a million listeners.
34
35
 So we do a number of things
36
 (*) use a cache-dense listener list (std::vector) because caching effects dominate a lot of operations
37
 (*) use a sorted list to speed up find operations
38
 (*) only sort the list when we absolutely have to, to speed up sequential add/remove operations
39
 (*) defer removing items from the list by (ab)using the last bit of the pointer
40
41
 Also we have some complications around destruction because
42
 (*) we broadcast a message to indicate that we are destructing
43
 (*) which can trigger arbitrality complicated behaviour, including
44
 (*) adding a removing things from the in-destruction object!
45
46
*/
47
48
static bool isDeletedPtr(SvtListener* p)
49
154M
{
50
    /** mark deleted entries by toggling the last bit,which is effectively unused, since the struct we point
51
     * to is at least 16-bit aligned. This allows the binary search to continue working even when we have
52
     * deleted entries */
53
154M
    return (reinterpret_cast<uintptr_t>(p) & 0x01) == 0x01;
54
154M
}
55
56
static void markDeletedPtr(SvtListener*& rp)
57
5.74M
{
58
5.74M
    reinterpret_cast<uintptr_t&>(rp) |= 0x01;
59
5.74M
}
60
61
static void sortListeners(std::vector<SvtListener*>& listeners, size_t firstUnsorted)
62
973k
{
63
    // Add() only appends new values, so often the container will be sorted expect for one
64
    // or few last items. For larger containers it is much more efficient to just handle
65
    // the unsorted part.
66
973k
    auto sortedEnd = firstUnsorted == 0
67
973k
        ? std::is_sorted_until(listeners.begin(),listeners.end())
68
973k
        : listeners.begin() + firstUnsorted;
69
973k
    if( listeners.end() - sortedEnd == 1 )
70
450k
    {   // Just one item, insert it in the right place.
71
450k
        SvtListener* item = listeners.back();
72
450k
        listeners.pop_back();
73
450k
        listeners.insert( std::upper_bound( listeners.begin(), listeners.end(), item ), item );
74
450k
    }
75
523k
    else if( o3tl::make_unsigned( sortedEnd - listeners.begin()) > listeners.size() * 3 / 4 )
76
324k
    {   // Sort the unsorted part and then merge.
77
324k
        std::sort( sortedEnd, listeners.end());
78
324k
        std::inplace_merge( listeners.begin(), sortedEnd, listeners.end());
79
324k
    }
80
198k
    else
81
198k
    {
82
198k
        std::sort(listeners.begin(), listeners.end());
83
198k
    }
84
973k
}
85
86
void SvtBroadcaster::Normalize() const
87
172M
{
88
    // clear empty slots first, because then we often have to do very little sorting
89
172M
    if (mnEmptySlots)
90
1.30M
    {
91
151M
        std::erase_if(maListeners, [] (SvtListener* p) { return isDeletedPtr(p); });
92
1.30M
        mnEmptySlots = 0;
93
1.30M
    }
94
95
172M
    if (mnListenersFirstUnsorted != static_cast<sal_Int32>(maListeners.size()))
96
970k
    {
97
970k
        sortListeners(maListeners, mnListenersFirstUnsorted);
98
970k
        mnListenersFirstUnsorted = maListeners.size();
99
970k
    }
100
101
172M
    if (!mbDestNormalized)
102
3.48k
    {
103
3.48k
        sortListeners(maDestructedListeners, 0);
104
3.48k
        mbDestNormalized = true;
105
3.48k
    }
106
172M
}
107
108
void SvtBroadcaster::Add( SvtListener* p )
109
9.89M
{
110
9.89M
    assert(!mbDisposing && "called inside my own destructor?");
111
9.89M
    assert(!mbAboutToDie && "called after PrepareForDestruction()?");
112
9.89M
    if (mbDisposing || mbAboutToDie)
113
0
        return;
114
    // Avoid normalizing if the item appended keeps the container sorted.
115
9.89M
    auto nRealSize = static_cast<sal_Int32>(maListeners.size() - mnEmptySlots);
116
9.89M
    auto bSorted = mnListenersFirstUnsorted == nRealSize;
117
9.89M
    if (maListeners.empty() || (bSorted && maListeners.back() <= p))
118
2.31M
    {
119
2.31M
        ++mnListenersFirstUnsorted;
120
2.31M
        maListeners.push_back(p);
121
2.31M
        return;
122
2.31M
    }
123
    // see if we can stuff it into an empty slot, which succeeds surprisingly often in
124
    // some calc workloads where it removes and then re-adds the same listener
125
7.58M
    if (mnEmptySlots && bSorted)
126
2.51M
    {
127
2.51M
        auto it = std::lower_bound(maListeners.begin(), maListeners.end(), p);
128
2.51M
        if (it != maListeners.end() && isDeletedPtr(*it))
129
1.93M
        {
130
1.93M
            *it = p;
131
1.93M
            ++mnListenersFirstUnsorted;
132
1.93M
            --mnEmptySlots;
133
1.93M
            return;
134
1.93M
        }
135
2.51M
    }
136
5.64M
    maListeners.push_back(p);
137
5.64M
}
138
139
void SvtBroadcaster::Remove( SvtListener* p )
140
7.59M
{
141
7.59M
    if (mbDisposing)
142
886k
        return;
143
144
6.70M
    if (mbAboutToDie)
145
962k
    {
146
        // only reset mbDestNormalized if we are going to become unsorted
147
962k
        if (!maDestructedListeners.empty() && maDestructedListeners.back() > p)
148
27.1k
            mbDestNormalized = false;
149
962k
        maDestructedListeners.push_back(p);
150
962k
        return;
151
962k
    }
152
153
    // We only need to fully normalize if one or more Add()s have been performed that make the array unsorted.
154
5.74M
    auto nRealSize = static_cast<sal_Int32>(maListeners.size() - mnEmptySlots);
155
5.74M
    if (mnListenersFirstUnsorted != nRealSize || (maListeners.size() > 1024 && maListeners.size() / nRealSize > 16))
156
830k
        Normalize();
157
158
5.74M
    auto it = std::lower_bound(maListeners.begin(), maListeners.end(), p);
159
5.74M
    assert (it != maListeners.end() && *it == p);
160
5.74M
    if (it != maListeners.end() && *it == p)
161
5.74M
    {
162
5.74M
        markDeletedPtr(*it);
163
5.74M
        ++mnEmptySlots;
164
5.74M
        --mnListenersFirstUnsorted;
165
5.74M
    }
166
167
5.74M
    if (!HasListeners())
168
1.68M
        ListenersGone();
169
5.74M
}
170
171
SvtBroadcaster::SvtBroadcaster( const SvtBroadcaster &rBC ) :
172
0
    mnEmptySlots(0), mnListenersFirstUnsorted(0),
173
0
    mbAboutToDie(false), mbDisposing(false),
174
0
    mbDestNormalized(true)
175
0
{
176
0
    assert(!rBC.mbAboutToDie && "copying an object marked with PrepareForDestruction()?");
177
0
    assert(!rBC.mbDisposing && "copying an object that is in its destructor?");
178
179
0
    rBC.Normalize(); // so that insert into ourself is in-order, and therefore we do not need to Normalize()
180
0
    maListeners.reserve(rBC.maListeners.size());
181
0
    for (SvtListener* p : rBC.maListeners)
182
0
         p->StartListening(*this); // this will call back into this->Add()
183
0
}
184
185
SvtBroadcaster::~SvtBroadcaster()
186
58.4M
{
187
58.4M
    mbDisposing = true;
188
58.4M
    Broadcast( SfxHint(SfxHintId::Dying) );
189
190
58.4M
    Normalize();
191
192
    // now when both lists are sorted, we can linearly unregister all
193
    // listeners, with the exception of those that already asked to be removed
194
    // during their own destruction
195
58.4M
    ListenersType::const_iterator dest(maDestructedListeners.begin());
196
58.4M
    for (auto& rpListener : maListeners)
197
4.15M
    {
198
        // skip the destructed ones
199
5.10M
        while (dest != maDestructedListeners.end() && (*dest < rpListener))
200
954k
            ++dest;
201
202
4.15M
        if (dest == maDestructedListeners.end() || *dest != rpListener)
203
3.18M
            rpListener->BroadcasterDying(*this);
204
4.15M
    }
205
58.4M
}
206
207
void SvtBroadcaster::Broadcast( const SfxHint &rHint )
208
112M
{
209
112M
    Normalize();
210
211
112M
    ListenersType::const_iterator dest(maDestructedListeners.begin());
212
112M
    ListenersType aListeners(maListeners); // this copy is important to avoid erasing entries while iterating
213
112M
    for (auto& rpListener : aListeners)
214
6.00M
    {
215
        // skip the destructed ones
216
6.96M
        while (dest != maDestructedListeners.end() && (*dest < rpListener))
217
954k
            ++dest;
218
219
6.00M
        if (dest == maDestructedListeners.end() || *dest != rpListener)
220
5.04M
            rpListener->Notify(rHint);
221
6.00M
    }
222
112M
}
223
224
1.68M
void SvtBroadcaster::ListenersGone() {}
225
226
SvtBroadcaster::ListenersType& SvtBroadcaster::GetAllListeners()
227
0
{
228
0
    Normalize();
229
0
    return maListeners;
230
0
}
231
232
const SvtBroadcaster::ListenersType& SvtBroadcaster::GetAllListeners() const
233
0
{
234
0
    Normalize();
235
0
    return maListeners;
236
0
}
237
238
void SvtBroadcaster::PrepareForDestruction()
239
496k
{
240
496k
    mbAboutToDie = true;
241
    // the reserve() serves two purpose (1) performance (2) makes sure our iterators do not become invalid
242
496k
    maDestructedListeners.reserve(maListeners.size());
243
496k
}
244
245
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */