Category Archives: Техника

Compiling OpenCV on mac OS 10.13.6 with CUDA

Just a little hint for myself:

cmake -DCMAKE_BUILD_TYPE=Release \
-DOPENCV_EXTRA_MODULES_PATH=<path to opencv contrib repo>/modules \
<path to opencv source>

Important things here:

  • OPENCV_CUDA_FORCE_BUILTIN_CMAKE_MODULE – is required, due to some weird staff in cmake (cmake internal FindCUDA.cmake seems to be wrong), this comment on opencv github by alalek was extemely usefull and provided us with right hint.
  • WITH_PROTOBUF is also required, otherwise it fails with crazy libprotobuf linker errors.
  • OPENCV_EXTRA_SHARED_LINKER_FLAGS=”-lomp” If you’re using clang 10 or older, you’ll need to to add “-lomp” to linker flags manually, because it ignores “-fopenmp” flag which cmake passes to clang driver in this case. Look’s like it falls just here:

  • Rest of flags looks pretty obvious
Please follow and like us:

Simple script to create Mac OS installation .iso media


It is supposed, that you downloaded it with App Store, and it is stored as .app in your /Applications directory.

Below is script. The only parameter you have to customize is a “DIST” variable, which should be your distributive name (“Mojave”, “Catalina” and so on).

This article is based on instructions from

As a result you should get .iso file on your desktop with name of your distributive (e.g. “Catalina.iso”).


INSTALL_APP="Install macOS $"
MEDIA_VOL="Install macOS $DIST"

echo Create dmg... &&
hdiutil create -o $DMG_FILE -size 8500m -volname $VOL_NAME \
       -layout SPUD -fs HFS+J &&

echo Attach dmg... &&
hdiutil attach $DMG_FILE_EXT -noverify -mountpoint $VOL_NAME &&

echo Create install media... &&
sudo /Applications/"$INSTALL_APP"/Contents/Resources/createinstallmedia \
    --volume $VOL_NAME --nointeraction &&

echo Detach $MEDIA_VOL... &&
hdiutil detach /Volumes/"$MEDIA_VOL" &&

echo Convert $DMG_FILE_EXT -\> $CDR_FILE &&
hdiutil convert $DMG_FILE_EXT -format UDTO -o $CDR_FILE &&

echo Rename $CDR_FILE -\> $ISO_FILE &&

echo Cleanup... &&
Please follow and like us:

Cross compilation for Raspberry from Sierra

In very short

If you need to compile something for raspberry just run this:

path/to/clang --target=arm-linux-gnueabihf --sysroot=/some/path/arm-linux-gnueabihf/sysroot my-happy-program.c  -fuse-ld=lld

In command above “arm-linux-gnueabihf” – is my target triple.

If you don’t like LLVM or just need GCC, read Yuzhou Cheng’s article . Or lookup in nets something like “cross compilation for raspberry”. This may help. Below we describe how to do it with LLVM.


We assume that reader knows how to deal with command line. If not, don’t worry, it’s ok, not to know some things in our life. Feel free and just ask questions in comments.

Let’s start

Root FS

Of course you still need rootfs. And also you perhaps need gcc binutils, but perhaps you would like to use ones provided by llvm infrastructure. But. You don’t have to build it, just get it, e.f. from Linux package. But actually I’m looking for solution how to make it enough just to mount my raspberry rootfs.

How to get LLVM

At current moment there are precompiled binaries for Mac OS (go to “Pre-Built Binaries” paragraph):

Or for version 7.0.0 you may run this in terminal:

$ wget

Compiling LLVM from sources

Don’t worry this is a bit different from building gcc. Difference is in statistics fact, that it usually successful and you can really drink cup of coffee.


Below are few brew commands which adds all dependencies we need.

$ brew install swig
$ brew install cmake

Get sources

Get LLVM, Clang, LLD and LLDB sources, once again same link:

Or for 7.0.0:

1. Extract LLVM sources.

2. Extract LLD into llvm/tools/lld

3. Extract LLDB into llvm/tools/lldb

4. Most tricky part: lldb needs to be code signed. This article describes how to to that. Actually you should find it in your lldb sources dir, in lldb/docs/code-signing.txt.

5. Create some binary dir, let say “llvm.darwin-x86_64”, and cd into it.

6. Compile

cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Release <path to llvm sources>

make -j<num-parallel jobs, for me it is 8>

7. Test it.

make -j8 check

8. Use it!

Post scriptum

Optionally you may use legacy binutils. In this case install them with brew:

$ brew install arm-linux-gnueabihf-binutils

But I prefer just to use single solution.

CMake Toolchain

Below is my cmake toolchain file, which uses clang (built from sources). Hope it will be useful for you.



# Custom toolchain-specific definitions for your project

# There we go!
# Below, we specify toolchain itself!

SET(TARGET_TRIPLE arm-linux-gnueabihf)

# Specify your target rootfs mount point on your compiler host machine

# Specify clang paths
SET(LLVM_DIR /Users/stepan/projects/shared/toolchains/llvm-7.0.darwin-release-x86_64/install)
SET(CLANG ${LLVM_DIR}/bin/clang)
SET(CLANGXX ${LLVM_DIR}/bin/clang++)

# Specify compiler (which is clang)

# Specify binutils

SET (CMAKE_AR      "${LLVM_DIR}/bin/llvm-ar" CACHE FILEPATH "Archiver")
SET (CMAKE_NM      "${LLVM_DIR}/bin/llvm-nm" CACHE FILEPATH "NM")
SET (CMAKE_OBJDUMP "${LLVM_DIR}/bin/llvm-objdump" CACHE FILEPATH "Objdump")
SET (CMAKE_RANLIB  "${LLVM_DIR}/bin/llvm-ranlib" CACHE FILEPATH "ranlib")

# You may use legacy binutils though.
#SET(BINUTILS /usr/local/Cellar/arm-linux-gnueabihf-binutils/2.31.1)

# Specify sysroot (almost same as rootfs)

# Specify lookup methods for cmake

# Sometimes you also need this:

# Specify raspberry triple
set(CROSS_FLAGS "--target=${TARGET_TRIPLE}")

# Specify other raspberry related flags

# Gather and distribute flags specified at prev steps.

# Use clang linker. Why?
# Well, you may install custom arm-linux-gnueabihf binutils,
# but then, you also need to recompile clang, with customized triple;
# otherwise clang will try to use host 'ld' for linking,
# so... use clang linker.

Sometimes you need to run “cmake” twice, for first compilation gives you this:

error: invalid linker name in argument '-fuse-ld=lld;-fuse-ld=lld'

I have no idea why that happens. Rerunning cmake really helps.

Ok, that’s it.

Message me if you feel lonely dude, I’m still on it, it will try to help!

Please follow and like us:

Разрешение e-ink в YotaPhone 2

Господа, я стал счастливым обладателем YotaPhone 2! И вот мой вывод – телефон удобен. Но e-ink у него недоделан. Сейчас создается впечатление, что я всетаки переплатил. Однако разработчики этой компании славятся быстрой реакцией и регулярными правками ПО.

Итак (внизу есть UPD c более качественными фотками).

Разрешение e-ink экрана.

В общем – полный бардак. Оно разное для разных приложений. То есть на программном уровне выставлено по разному. Как так?


Самое лучшее, что мне удалось получить – это изображение для YotaCover. Там действительно можно наблюдать 235 dpi. Хотя и с некоторыми затруднениями.
Дело в том, что контрастность изображения настолько низкая, что его можно сравнить только, разве что, с первыми электронными книгами. Когда я положил рядом PocketBook 623 Touch 2 с разрешением 212 dpi, то у YotaPhone 2 не было просто никаких шансов. При более низком разрешении качество первого казалось просто полиграфическим. В то время, как на экране YotaPhone все было немного размыто. Я приложил сравнительный снимки. Для понимания масштаба положил  сверху волос.

 PocketBook 623 Touch 2, 212 dpi
YotaPhone 2, 235 dpi

Да – у камеры другое фокусное расстояние в отличие от глаза, однако оно одинаково как для книги, так и для телефона, и отражает суть: минимальное расстояние перехода от полностью черной области экрана полностью белой на телефоне больше, а стало быть – контрастность ниже.
В чем причина низкой контрастности? Матовый экран? Но ведь даже ghost-effect  на вашем устройстве выглядит четче, чем само изображение. Возможно вы неправильно выставили разрешение; возможно – это качество самого e-ink экрана. Я все-таки надеюсь, что это проблема с разрешением и ее можно решить.

Комфортное чтение

Так вот – оно не комфортное. Разрешение в этом режиме почти В ДВА РАЗА НИЖЕ. Это что, от первого YotaPhone настройки? Неужели было сложно конфиг подправить?
В общем. Тот, кто хоть раз читал с нормальной e-книги, останется очень недовольным качеством текста. Я также прилагаю снимок. Обратите внимание, как рисуются границы символов букв “т” и “д”: какими-то страшными точками.
Замечу, что если сделать скрин-шот самим телефоном, и потом посмотреть его на LED дисплее, то выглядит все очень четко, собственно – именно так, как и должно быть при 235 dpi. (При просмотре на LED дисплее, разумеется, нужно сделать небольшой zoom скриншота, чтоб получилось на весь экран.)

Качество шрифта (на самом деле кривое разрешение)


Аналогично. Качество – как у матричных принтеров времен 90-х. Зачем рисовать картинки методом Монте-Карло? У вас 16 оттенков серого; этого более, чем достаточно. Классическая отрисовка экономичнее для батареи, красивее и четче.
Почему экономичнее? Если на светлом фоне в следующем кадре появляется объект, то вероятностная модель отрисовки (Монте-Карло) потребует как перерисовки самого объекта, так и перераспеделения точек на самом фоне: посмотрите как у вас песочные часы рисуются (на самом деле “песочное кольцо” 🙂 ). В общем избавтесь от этого кошмара, выполняйте рендеринг обычным способом. Будет гораздо красивее, будет меньше лишних вопросов, и, вдобавок – будет экономичнее.
Если кому-то нравятся эксперименты, то можно было бы сделать этот вид отрисовки дополнительной опцией.


А так получается, что вроде как разрешение экрана хорошее, но это – где-то там глубоко внутри устройства, а наблюдаем мы (пользователи) изображение очень посредственного качества.

UPD от 27 мая 2015

Вышло обновление до 5-го андроида, но рендеринг не исправили! Когда? В общем случайно на второй экран попала капля воды. Вода, имеет тот же коэффициент преломления, что и сам матовый экран, поэтому через каплю воды стало видно все.
Вот так рендерится текст в YotaReader.

omg, 21st century…

Вот более детальные снимки рендеринга в режиме YotaMirror.

(немножко мыльная капля, чтоб лучше растекалась)

В общем все видно и без воды здесь. Весь экран в крапинку. Люди! Как вы это терпите? Почему я один возмущаюсь?

Как должно быть

YotaSnapshot иногда выполняет правильный рендеринг. Поэтому сделав Snapshot основного экрана, удалось увидеть “правильный” вариант для YotaMirror.

 Заметьте что все гладко, и даже под каплей воды никаких “веснушек” не наблюдается.

Заключение оставлю прежним. Уважаемые разработчики YotaPhone 2, этот пост адресован в первую очередь вам. Сделайте, пожалуйста, что-нибудь. В общем-то непонятен подход. Этот пост ежедневно смотрят не меньше 50-ти человек.

С уважением,
Дятковский Степан.

Please follow and like us:

LLVM, MergeFunctions pass


**** MergeFunctions pass ****

Sometimes code contains functions that does exactly the same things even though they are non-equal on binary level. It could happen due to several reasons: mainly the usage of templates and automatic code generators. Though sometimes user itself could write same thing twise 🙂
The main purpose of pass is to recognize equal functions and merge them.

*** MergeFunctions, main fields and runOnModule ***

The are two key fields in class:
FnSet – the set of all unique functions. It keeps items that couldn’t be merged with each other.
Deferred – merging process can affect bodies of functions that are in FnSet already. These functions should be checked again. In that case we remove them from FnSet, and mark then as to be analyzed again: put them into Deferred list.

** runOnModule **

The algorithm is pretty simple:
1. Put all module’s functions into worklist.
2. Scan worklist’s functions twice: first enumerate only strong functions and then only weak functions:
2.1. (inside loop body) Take function from worklist and try to insert it into FnSet: check whether it equal to one of functions in FnSet. If there is equal function in FnSet: merge function token from worklist with that equal function from FnSet. Otherwise add function from worklist to FnSet.
3. After worklist scanning and merging operations complete, check Deferred list. If it is not empty: refill worklist contents with Deferred list and do 2 again, or exit from method otherwise (Deferred is empty).

* Narrative structure *

Article consists of two parts. First part describes comparison procedure itself. The second one describes the merging process.
Description will be in top-down form. First, top-level methods will be described. While the terminal ones will be at the end, in the tail of each part.
Few more words about top-level and complex objects comparison. Complex objects comparison function, basic-block, etc) is mostly based on its sub-objects comparison results. So, again, if reader will see the reference to method that wasn’t described yet, he will find its description a bit below.

*** Merge Functions Pass Comparison Algorithm, “compare” method ***

Comparison starts in “FunctionComparator::compare” method.
1. First parts to be compared are function attributes and some properties that outsides “attributes” term, but still could make function different without changing its body. This part of comparison is done within simple == operator (e.g. F1->hasGC() == F2->hasGC()). There are full list of function properties to be compared on this stage:
* Attributes (those are returned by Function::getAttributes() method).
* GC, for equivalence, RHS and LHS should be both either without GC or with the same one.
* Section, like a GC, RHS and LHS should be defined in the same section.
* Variable arguments. If LHS and RHS should be both either with or without var-args.
* Calling convention should be the same.
2. Function type.
Checked by FunctionComparator::isEquivalentType(Type*, Type*) method. It checks return type and parameters type; the method itself will be described later.
3. Associate function formal parameters with each other. Then during stage of function bodies, if we see usage of 1st argument from LEFT function, we want to see it in RIGHT at the same place, otherwise functions are different. This is done by “FunctionComparator::enumerate(const Value*, const Value*)” method (will be described a bit later).
4. Function body comparison. As its written in method comments:
“We do a CFG-ordered walk since the actual ordering of the blocks in the linked list is immaterial. Our walk starts at the entry block for both functions, then takes each block from each terminator in order. As an artifact, this also means that unreachable blocks are ignored.”
So, using this walk we get BBs from LEFT and RIGHT in the same order, and compare them by “FunctionComparator::compare(const BasicBlock*, const BasicBlock*)” method.
We also associate BBs with each other like we did with function formal arguments: Then if we meet reference to basic block “A” in LHS, we want to see reference to “A`” in RHS at the same place, and “A`” ought to be associated with “A”. Otherwise functions are different.

** FunctionComparator::isEquivalentType **

Let’s describe this comparison in six steps.
0. If left type is pointer, try to coerce it to integer type. It could be done if its address space is 0, or if address spaces are ignored at all. Do the same thing for right type.
1. It returns true if left and right types are equal:
“if (LeftTy == RightTy) return true;”
2. If types are of different kind (different type IDs). Return “false”.
Below cases when we have same type IDs goes.
3. If types are vectors or integers, return result of its pointers comparison.
4. Check left type by its ID, whether it belongs to the next group (call it equivalent-group):
* Void
* Float
* Double
* X86_FP80
* FP128
* PPC_FP128
* Label
* Metadata
Method treats LEFT and RIGHT as equals (return true). Since in that case its enough to see equivalent IDs. Note, if left belongs to this group, while right doesn’t, or right just has different typeID we return “false”.
5. Left and right are pointers, then they are equal if and only if they belongs to the same address space.
6. If left type is complex (structure, function or array, whatever else), and if right type is of the same kind.
Then both LEFT and RIGHT will be expanded and their element types will be checked with the same way.
Method treats them as equal if they are of the same kind and all their element types are equal as well.
7. Otherwise method returns “false”. Even if types has the same TypeID, we can’t treat them as equals. Instead there are now other cases, and its point to put llvm_unreachable call.
Special note about case with pointers and integers. Its a point of false-positive now. Consider next case on 32bit machine:
void foo0(i32 addrespace(1)* %p)
void foo1(i32 addrespace(2)* %p)
void foo2(i32 %p)
Here: foo0 != foo1, while
foo1 == foo2 and foo0 == foo2.
As you can see it breaks transitivity. That means that result depends on order of how functions are presented in module. Next order causes merging of foo0 and foo1:
foo2, foo0, foo1
First foo0 will be merged with foo2, foo0 will be erased. Second foo1 will be merged with foo2.
This case looks like a bug and it is under discussion now (see PR17925).

** FunctionComparator::enumerate(const Value*, const Value*) **

Main purpose is to associate Value from left with Value from right. If we see usage of Value “A” at left, we expect to see usage of “A`” at right, at the same place, and we also expect to see “A`” associated with “A”.
Method returns “true” if values are associated already (implicitly by its nature, or explicitly by helper structures in MergeFunction pass).
Method returns “false” if values could not be associated. It indicates to caller-side, that things are being compared could not be equal.
We associate (we use “enumerate” for):
* Function arguments. i-th argument from left function associated with i-th argument from right function.
* BasicBlock instances. In basic-block enumeration loop we associate i-th BasicBlock from LEFT with i-th BasicBlock from RIGHT.
* Instructions.
* Instruction operands. Note, we can meet Value here we have never seen before. In this case it is not function argument nor BasicBlock, nor Instruction. It is global value. That means it is constant (its supposed to be seen here, at least). Method only accepts as equal next:
* Constants that are of the same type and value
* Right constant could be losslessly bit-casted to the left.
Otherwise method returns “false”.
Below is the detailed method body description.
Method performs next four things:
1. If left Value is left/right Function instance, then right Value should be right/left Function instance. If so: return true.
Note we return true for self-reference, and for cross-reference, in example below fact0 is equal to fact1, and ping is equal to pong as well:
// self-reference
unsigned fact0(unsigned n) { return n > 1 ? n * fact0(n-1) : 1; }
unsigned fact1(unsigned n) { return n > 1 ? n * fact1(n-1) : 1; }
// cross-reference
unsigned ping(unsigned n) { return n!= 0 ? pong(n-1) : 0; }
unsigned pong(unsigned n) { return n!= 0 ? ping(n-1) : 0; }
Though, the ping-pong case is pretty seldom in real live.
Otherwise we go to next stage.
2. If left Value is constant. Method returns true in cases:
* Right one is the same constant,
* Both LEFTt and RIGHT are null values of the same type (it invokes isEquivalentType method),
* RIGHT could be losslessly bit-casted to the LEFT.
Otherwise method returns “false”.
3. If left is InlineAsm instance. The right should be the same instance then; if so: we return true. Otherwise return false.
4. Explicit association of L (left value) and R (right value).
Now follow the logic. We can associate values were not associated before. New values for us. Since “enumerate” is called for values that stays at the same place of their functions, we met them first at the same place. It is important.
MergeFunction pass has two helper data structures:
* id_map – is map of format map. Keeps track of all associated values. With left value as a key.
* seen_values – set of right values for whom there already was attempt to create an association.
On this stage method checks id_map[L].
* If it is not null, L is already associated with something, the result of id_map[L] == R comparison is returned in this case.
* If it is null, then we see this value first time, if R was not associated yet (seen_values.insert(R) returns “true”), we do the association: setup R as value for id_map[L].
Otherwise: there is still no association for L, but R was associated before, so method returns “false”.

*** compare(const BasicBlock*, const BasicBlock*) ***

Compares two BasicBlock instances.
It enumerates instructions from left BB and right BB.
1. It associates left instruction with right one, using “enumerate” method.
2. If left is GEP, it compares them using isEquivalentGEP method. Since we have some optimizations for this case.
3. Otherwise method ensures that LEFT and RIGHT performs the same operation (isEquivalentOperation) and its operands are equal: left operand should be properly associated with right one, and it should be of the same type (isEquivalentType).

*** isEquivalentGEP ***

Compares two GEPs.
There is an optimization for case, where offset is provided by constant values for both left and right GEPs. We calculate final offset for both of them using accumulateConstantOffset method. If we got same offset for left and right: return true.
Otherwise we don’t know what the final offset is. Compare GEP’s operands (as we do for all other instructions).

*** isEquivalentOperation ***

Compares instruction opcodes and some important operation properties.
It returns false in one of next cases:
* opcodes are different,
* number of operands are different,
* operation types are different,
* operation optional flags are different (checked by hasSameSubclassOptionalData method),
* operand types are different.
* Also for some particular instructions it checks equivalence of some significant attributes (`load`, `store`, `cmp`, `call, `invoke`, see method contents for full list).
For example for `load` left and right should be with the same alignment.

*** Merging process, mergeTwoFunctions ***

Once MergeFunctions found that current function (“G”) is equal to one that were analyzed before (function “F”) it calls mergeTwoFunctions(Function*, Function*).
Operation affects FnSet contents with next way: “F” will stay in FnSet. “G” being equal to “F” will not be added to FnSet. Calls of “G” would be replaced with something else. It changes bodies of callers. So, functions that calls “G” would be put into Deferred set and removed from FnSet, and analyzed again.
The approach is next:
1. If we can use alias and both of “F” and “G” are weak. It is most wished case. We make both of them with aliases to third strong function “H”. Actually “H” is “F”. See below how its made. In case when we can just replace “G” with “F” everywhere, we can use replaceAllUsesWith operation.
2. “F” could not be overriden, while “G” could. It would be good to do the next: after merging the places where overridable function where used, still use overridable stub.
So try to make “G” alias to “F”, or create overridable tail call wrapper around “F” and replace “G” with that call.
3. Neither “F” nor “G” could be overridden. We can’t use RAUW. We can just change the callers: call “F” instead of “G”. That’s what replaceDirectCallers does.
Below is detailed body description.

** If “F” may be overriden **

As follows from mayBeOverridden comments: “whether the definition of this global may be replaced by something non-equivalent at link time”. If so, thats ok: we can use alias to “F” instead of “G” or change call instructions itself.

* HasGlobalAliases, removeUsers *

First consider the case when we have global aliases of one function name to another. Our purpose is make both of them with aliases to third strong function. Though if we keep “F” alive and without major changes we can leave it in FnSet. Try to combine these two goals.
Do stub replacement of “F” itself with an alias to “F”.
1. Create stub function “H”, with the same name and attributes like function “F”. It takes maximum alignment of “F” and “G”.
2. Replace all uses of function “F” with uses of function “H”. It is two steps procedure instead. First of all, we must take into account, all functions from whom “F” is called, will be changed: since we change the call argument (from “F” to “H”). If so we must to review these caller functions again after this procedure. We remove callers from FnSet, that’s why we call “removeUsers(F)”.
2.1. Inside removeUsers(Value* V) we go through the all values that use value “V” (or “F” in our context). If value is instruction, we go to function that holds this instruction and mark it as to-be-analyzed-again (put to Deferred set), we also remove caller from FnSet.
2.2. Now we can do the replacement: call F->replaceAllUsesWith(H).
3. Get rid of “G”, and get rid of “H”.
4. Set “F” linkage to private. Make it strong 🙂

* No global aliases, replaceDirectCallers *

If global aliases are not supported. We call replaceDirectCallers then. Just go through all calls of “G” and replace it with calls of “F”. If you look into method you will see that it scans all uses of “G” too, and if use is callee (if user is call instruction and “G” is used as what to be called), we replace it with use of “F”.

** If “F” could not be overriden, writeThunkOrAlias **

We call writeThunkOrAlias(Function *F, Function *G). Here we try to replace “G” with alias to “F” first. Next conditions are essential:
* target should support global aliases,
* the address itself of “G” should be not significant, not named and not referenced anywhere,
* function should come with external, local or weak linkage.
Otherwise we write thunk: some wrapper that has “G”s interface and calls “F”, so “G” could be replaced with this wrapper.

* writeAlias(Function *F, Function *G) *

As follows from llvm reference:
“Aliases act as “second name” for the aliasee value”. So we just want to create second name for “F” and use it instead of “G”:
1. create global alias itself (“GA”),
2. adjust alignment of “F” so it must be max of current and “G”s alignment;
3. replace uses of “G”:
3.1. first mark all callers of “G” as to-be-analyzed-again, using removeUsers method (see chapter above),
3.2. call G->replaceAllUsesWith(GA).
4. Get rid of “G”.

* writeThunk(Function *F, Function *G) *

As it written in method comments:
“Replace G with a simple tail call to bitcast(F). Also replace direct uses of G with bitcast(F). Deletes G.”
In general it does the same as usual when we want to replace callee, except the first point:
1. We generate tail call wrapper around “F”, but with interface that allows use it instead of “G”.
2. “As-usual”: removeUsers and replaceAllUsesWith then.
3. Get rid of “G”.

*** That’s it. ***

If you have some questions or additions, please let me know.
Please follow and like us:

Вчера почистил свои компы.

Вот этот примерно 1.5 года не чистил:

Слой пылевого валенка составил примерно 2 мм.

А вот этот – со дня покупки, а купил я его примерно в 2007 году.

“Налет” местами до 1 см!

Вывод: проделав это работу… можно заключить, что за год на радиаторе оседает примерно 1.5 мм. пыли, а где-то через 4 года быль на радиаторе начинает уплотняться и ее можно даже использовать для какого-нибудь ремесла (валенки для домашней свинки и т.д.).

Please follow and like us: