constructor_stats.h 10.9 KB
Newer Older
1
2
#pragma once
/*
Dean Moldovan's avatar
Dean Moldovan committed
3
    tests/constructor_stats.h -- framework for printing and tracking object
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
    instance lifetimes in example/test code.

    Copyright (c) 2016 Jason Rhinelander <jason@imaginary.ca>

    All rights reserved. Use of this source code is governed by a
    BSD-style license that can be found in the LICENSE file.

This header provides a few useful tools for writing examples or tests that want to check and/or
display object instance lifetimes.  It requires that you include this header and add the following
function calls to constructors:

    class MyClass {
        MyClass() { ...; print_default_created(this); }
        ~MyClass() { ...; print_destroyed(this); }
        MyClass(const MyClass &c) { ...; print_copy_created(this); }
        MyClass(MyClass &&c) { ...; print_move_created(this); }
        MyClass(int a, int b) { ...; print_created(this, a, b); }
        MyClass &operator=(const MyClass &c) { ...; print_copy_assigned(this); }
        MyClass &operator=(MyClass &&c) { ...; print_move_assigned(this); }

        ...
    }

Jason Rhinelander's avatar
Jason Rhinelander committed
27
You can find various examples of these in several of the existing testing .cpp files.  (Of course
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
you don't need to add any of the above constructors/operators that you don't actually have, except
for the destructor).

Each of these will print an appropriate message such as:

    ### MyClass @ 0x2801910 created via default constructor
    ### MyClass @ 0x27fa780 created 100 200
    ### MyClass @ 0x2801910 destroyed
    ### MyClass @ 0x27fa780 destroyed

You can also include extra arguments (such as the 100, 200 in the output above, coming from the
value constructor) for all of the above methods which will be included in the output.

For testing, each of these also keeps track the created instances and allows you to check how many
of the various constructors have been invoked from the Python side via code such as:

Jason Rhinelander's avatar
Jason Rhinelander committed
44
    from pybind11_tests import ConstructorStats
45
46
47
48
49
50
51
52
53
54
55
56
57
58
    cstats = ConstructorStats.get(MyClass)
    print(cstats.alive())
    print(cstats.default_constructions)

Note that `.alive()` should usually be the first thing you call as it invokes Python's garbage
collector to actually destroy objects that aren't yet referenced.

For everything except copy and move constructors and destructors, any extra values given to the
print_...() function is stored in a class-specific values list which you can retrieve and inspect
from the ConstructorStats instance `.values()` method.

In some cases, when you need to track instances of a C++ class not registered with pybind11, you
need to add a function returning the ConstructorStats for the C++ class; this can be done with:

59
    m.def("get_special_cstats", &ConstructorStats::get<SpecialClass>, py::return_value_policy::reference)
60
61
62
63
64
65
66

Finally, you can suppress the output messages, but keep the constructor tracking (for
inspection/testing in python) by using the functions with `print_` replaced with `track_` (e.g.
`track_copy_created(this)`).

*/

Dean Moldovan's avatar
Dean Moldovan committed
67
#include "pybind11_tests.h"
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#include <unordered_map>
#include <list>
#include <typeindex>
#include <sstream>

class ConstructorStats {
protected:
    std::unordered_map<void*, int> _instances; // Need a map rather than set because members can shared address with parents
    std::list<std::string> _values; // Used to track values (e.g. of value constructors)
public:
    int default_constructions = 0;
    int copy_constructions = 0;
    int move_constructions = 0;
    int copy_assignments = 0;
    int move_assignments = 0;

    void copy_created(void *inst) {
        created(inst);
        copy_constructions++;
    }
Wenzel Jakob's avatar
Wenzel Jakob committed
88

89
90
91
92
    void move_created(void *inst) {
        created(inst);
        move_constructions++;
    }
Wenzel Jakob's avatar
Wenzel Jakob committed
93

94
95
96
97
    void default_created(void *inst) {
        created(inst);
        default_constructions++;
    }
Wenzel Jakob's avatar
Wenzel Jakob committed
98

99
100
    void created(void *inst) {
        ++_instances[inst];
Wenzel Jakob's avatar
Wenzel Jakob committed
101
102
    }

103
104
    void destroyed(void *inst) {
        if (--_instances[inst] < 0)
Wenzel Jakob's avatar
Wenzel Jakob committed
105
106
107
            throw std::runtime_error("cstats.destroyed() called with unknown "
                                     "instance; potential double-destruction "
                                     "or a missing cstats.created()");
108
109
    }

Wenzel Jakob's avatar
Wenzel Jakob committed
110
    static void gc() {
111
        // Force garbage collection to ensure any pending destructors are invoked:
Wenzel Jakob's avatar
Wenzel Jakob committed
112
113
114
115
116
117
118
119
120
121
122
#if defined(PYPY_VERSION)
        PyObject *globals = PyEval_GetGlobals();
        PyObject *result = PyRun_String(
            "import gc\n"
            "for i in range(2):"
            "    gc.collect()\n",
            Py_file_input, globals, globals);
        if (result == nullptr)
            throw py::error_already_set();
        Py_DECREF(result);
#else
123
        py::module_::import("gc").attr("collect")();
Wenzel Jakob's avatar
Wenzel Jakob committed
124
125
126
127
128
#endif
    }

    int alive() {
        gc();
129
        int total = 0;
Wenzel Jakob's avatar
Wenzel Jakob committed
130
131
132
        for (const auto &p : _instances)
            if (p.second > 0)
                total += p.second;
133
134
135
136
137
138
139
140
141
142
143
        return total;
    }

    void value() {} // Recursion terminator
    // Takes one or more values, converts them to strings, then stores them.
    template <typename T, typename... Tmore> void value(const T &v, Tmore &&...args) {
        std::ostringstream oss;
        oss << v;
        _values.push_back(oss.str());
        value(std::forward<Tmore>(args)...);
    }
Dean Moldovan's avatar
Dean Moldovan committed
144
145

    // Move out stored values
146
147
148
    py::list values() {
        py::list l;
        for (const auto &v : _values) l.append(py::cast(v));
Dean Moldovan's avatar
Dean Moldovan committed
149
        _values.clear();
150
151
152
153
154
155
156
157
158
159
160
        return l;
    }

    // Gets constructor stats from a C++ type index
    static ConstructorStats& get(std::type_index type) {
        static std::unordered_map<std::type_index, ConstructorStats> all_cstats;
        return all_cstats[type];
    }

    // Gets constructor stats from a C++ type
    template <typename T> static ConstructorStats& get() {
Wenzel Jakob's avatar
Wenzel Jakob committed
161
162
163
#if defined(PYPY_VERSION)
        gc();
#endif
164
165
166
167
168
169
170
171
        return get(typeid(T));
    }

    // Gets constructor stats from a Python class
    static ConstructorStats& get(py::object class_) {
        auto &internals = py::detail::get_internals();
        const std::type_index *t1 = nullptr, *t2 = nullptr;
        try {
172
            auto *type_info = internals.registered_types_py.at((PyTypeObject *) class_.ptr()).at(0);
173
174
175
176
177
178
179
180
181
182
            for (auto &p : internals.registered_types_cpp) {
                if (p.second == type_info) {
                    if (t1) {
                        t2 = &p.first;
                        break;
                    }
                    t1 = &p.first;
                }
            }
        }
183
        catch (const std::out_of_range&) {}
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
        if (!t1) throw std::runtime_error("Unknown class passed to ConstructorStats::get()");
        auto &cs1 = get(*t1);
        // If we have both a t1 and t2 match, one is probably the trampoline class; return whichever
        // has more constructions (typically one or the other will be 0)
        if (t2) {
            auto &cs2 = get(*t2);
            int cs1_total = cs1.default_constructions + cs1.copy_constructions + cs1.move_constructions + (int) cs1._values.size();
            int cs2_total = cs2.default_constructions + cs2.copy_constructions + cs2.move_constructions + (int) cs2._values.size();
            if (cs2_total > cs1_total) return cs2;
        }
        return cs1;
    }
};

// To track construction/destruction, you need to call these methods from the various
// constructors/operators.  The ones that take extra values record the given values in the
// constructor stats values for later inspection.
template <class T> void track_copy_created(T *inst) { ConstructorStats::get<T>().copy_created(inst); }
template <class T> void track_move_created(T *inst) { ConstructorStats::get<T>().move_created(inst); }
template <class T, typename... Values> void track_copy_assigned(T *, Values &&...values) {
    auto &cst = ConstructorStats::get<T>();
    cst.copy_assignments++;
    cst.value(std::forward<Values>(values)...);
}
template <class T, typename... Values> void track_move_assigned(T *, Values &&...values) {
    auto &cst = ConstructorStats::get<T>();
    cst.move_assignments++;
    cst.value(std::forward<Values>(values)...);
}
template <class T, typename... Values> void track_default_created(T *inst, Values &&...values) {
    auto &cst = ConstructorStats::get<T>();
    cst.default_created(inst);
    cst.value(std::forward<Values>(values)...);
}
template <class T, typename... Values> void track_created(T *inst, Values &&...values) {
    auto &cst = ConstructorStats::get<T>();
    cst.created(inst);
    cst.value(std::forward<Values>(values)...);
}
template <class T, typename... Values> void track_destroyed(T *inst) {
    ConstructorStats::get<T>().destroyed(inst);
}
template <class T, typename... Values> void track_values(T *, Values &&...values) {
    ConstructorStats::get<T>().value(std::forward<Values>(values)...);
}

230
231
232
233
234
235
236
237
238
239
240
/// Don't cast pointers to Python, print them as strings
inline const char *format_ptrs(const char *p) { return p; }
template <typename T>
py::str format_ptrs(T *p) { return "{:#x}"_s.format(reinterpret_cast<std::uintptr_t>(p)); }
template <typename T>
auto format_ptrs(T &&x) -> decltype(std::forward<T>(x)) { return std::forward<T>(x); }

template <class T, typename... Output>
void print_constr_details(T *inst, const std::string &action, Output &&...output) {
    py::print("###", py::type_id<T>(), "@", format_ptrs(inst), action,
              format_ptrs(std::forward<Output>(output))...);
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
}

// Verbose versions of the above:
template <class T, typename... Values> void print_copy_created(T *inst, Values &&...values) { // NB: this prints, but doesn't store, given values
    print_constr_details(inst, "created via copy constructor", values...);
    track_copy_created(inst);
}
template <class T, typename... Values> void print_move_created(T *inst, Values &&...values) { // NB: this prints, but doesn't store, given values
    print_constr_details(inst, "created via move constructor", values...);
    track_move_created(inst);
}
template <class T, typename... Values> void print_copy_assigned(T *inst, Values &&...values) {
    print_constr_details(inst, "assigned via copy assignment", values...);
    track_copy_assigned(inst, values...);
}
template <class T, typename... Values> void print_move_assigned(T *inst, Values &&...values) {
    print_constr_details(inst, "assigned via move assignment", values...);
    track_move_assigned(inst, values...);
}
template <class T, typename... Values> void print_default_created(T *inst, Values &&...values) {
    print_constr_details(inst, "created via default constructor", values...);
    track_default_created(inst, values...);
}
template <class T, typename... Values> void print_created(T *inst, Values &&...values) {
    print_constr_details(inst, "created", values...);
    track_created(inst, values...);
}
template <class T, typename... Values> void print_destroyed(T *inst, Values &&...values) { // Prints but doesn't store given values
    print_constr_details(inst, "destroyed", values...);
    track_destroyed(inst);
}
template <class T, typename... Values> void print_values(T *inst, Values &&...values) {
    print_constr_details(inst, ":", values...);
    track_values(inst, values...);
}