powered by simpleCommunicator - 2.0.61     © 2026 Programmizd 02
Целевая тема:
Создать новую тему:
Автор:
Закрыть
Цитировать
Форумы / Программирование [игнор отключен] [закрыт для гостей] / Тяпничная со-программа
25 сообщений из 54, страница 1 из 3
Тяпничная со-программа
    #39294661
Фотография mayton
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
Добрый день коллеги. С пятницей.

Я долго думал куда поместить этот сабж. В С++, Java или программинг. И решил все таки здесь т.к.
тема достаточно общая и о ней стоит говорить сразу в контексте многих языков.

Тема - со-программами или co-routines

Я квотирую определение с вики чтоб было понятно

wikiСопрограмма (англ. coroutine) — компонент программы, обобщающий понятие подпрограммы,
который дополнительно поддерживает множество входных точек (а не одну как подпрограмма),
остановку и продолжение выполнения с сохранением определённого положения.

Сопрограммы являются более гибкими и обобщёнными, чем подпрограммы, но реже используются
на практике. Применение сопрограмм являлось методикой ещё ассемблера, практиковалось
лишь в некоторых высокоуровневых языках (Simula, Modula-2). Сопрограммы хорошо
пригодны для реализации многих похожих компонентов программ (итераторов, бесконечных
списков, каналов, совместных задач).


Под катом - мотивация сабжа для меня.

Год назад я пытался нарисовать картинку Тяпничная география
- распределение Ipv4 адресов по странам и отображение этого
на диаграмме в виде цветных квадратов.

Это была не бизнес разработка и чуть позже она мне надоела и я ее отложил.

Для рисования квадратов я кодил заполняющую кривую Гилберта. И резальтатом разработок
была экспериментальная консольная тулза https://sourceforge.net/p/countryipdiagram/code/HEAD/tree/trunk/utils/src/gilbertroute.c
которая должна быть генератором координат Гилберта. Сами коды выводились в stdout для передачи в другую утилиту.

Но работать с pipeline-ом процессов неудобно. Мне нужна была генерализация алгоритма в виде шаблона алгорима
чтоб я мог его подставлять в любой код.

В чем была сложность? В рекурсивности самого алгоритма. Как вы знаете, писать рекурсивный итератор - довольно неудобно.
Нужно либо создать конечный автомат со стеком и работать с ним. (Это нетривиально и чревато ошибками). Кроме того большинство
стандартных алгоритмов доступных в сорцах уже изначально написаны под рекурсию и их удобно заимствовать в том виде как есть
без всяких приседаний и переписываний.

Второй вариант - использовать механизмы очередей (на базе BlockingQueue в Java к примеру) для того чтобы иммитировать поведение
итератора с рекурсией. Способ оригинальный. Но есть недостаток. Мы вводим еще один вычислительный поток в задачу которая по сути
является однопоточной. Отсюда автоматом - структуры данных которым нужна синхронизация (BlockingQueue) и накладные.

Вобщем нужен гладкий и удобный механизм разворачивания рекурсивных функций в генераторы.

Вобщем как обстоят дела с со-программами (co-routines) в С/С++/C#/Java/Groovy/Scala/JavaScript/Delphi.


Прошу прощения если я не упомянул ваш любимый язык X. Я просто не успел просмотреть обзоры фичей всех ваших языков.


Я вот сделал некоторую табличку в которой попробовал обрисовать положение вещей на текущий момент

Поддержка со-программ в ЯП
LanguageCo-routines supportCno supportС++Возможно с поддержкой boost::coroutinesC#with yield returnJavano suportGroovy?Scalawith yieldDelphino supportJava Script Since ECMA6
В многочисленных статьях также упоминается что для поддежки co-routines язык
или среда должен поддерживать continuations.

Вот такая ситуация. У кого какие были практики с coroutines? Поделитесь.
...
Рейтинг: 0 / 0
Тяпничная со-программа
    #39294711
Вася Уткин
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Гость
Было бы круто, если бы был полностью рабочий локализованный пример, который использует сопрограммы или который не использует, но при их наличии был бы намного понятнее/быстрее, на любом из упомянутых языков на онлайн компиляторе: http://ideone.com/

Так было бы намного наглядней. Пока что ни разу не встречал такого примера. Либо примеры все усложняют, либо упрощают, но используют какие-то гипотетические сторонние функции, которые почему-то не могут сохранить свое текущее состояние: ни в передаваемую структуру, ни в объект членом которого они являются, ни в стороннюю структуру куда можно обратиться по хэндлеру.

Нафига выходить по yield, выходи по return, а текущее состояние всех итераторов храни как члены класса, сохранятся в объекте.
Если просто хочется распараллеливания, то используй потоки, выходи по yield(), и используй потокобезопасный обмен - ровно теже сопрограммы.
...
Рейтинг: 0 / 0
Тяпничная со-программа
    #39294718
Фотография Anatoly Moskovsky
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
maytonУ кого какие были практики с coroutines? Поделитесь.
Используем в продакшене Boost.Coroutine (для асинхронных операций).
Код на порядок проще и понятнее, чем с колбэками.
...
Рейтинг: 0 / 0
Тяпничная со-программа
    #39294720
kealon(Ruslan)
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
mayton,
а goto отменили что ли уже?

как то давно, когда студентом был, делал нитевую многопоточность на TurboPascal-е, в общем-то такую вещь там можно было реализовать.

каналы как абстракция делаются в любом из языков поддерживающих генерики или метапрограммирование, зачем для этого делать языковые конструкции?
...
Рейтинг: 0 / 0
Тяпничная со-программа
    #39294721
Фотография Anatoly Moskovsky
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
Вася УткинНафига выходить по yield, выходи по return, а текущее состояние всех итераторов храни как члены класса, сохранятся в объекте.
Так написал же человек - не хочет он явно хранить состояние, когда алгоритмически никакого состояния нет.
Т.е. то что вы предлагаете, это усложнение алгоритма.
...
Рейтинг: 0 / 0
Тяпничная со-программа
    #39294722
Фотография mayton
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
Я щас сходу не готов дать охватывающий пример. У меня щас нет такого под рукой.

Но я могу привести контр-пример. Возьмите любую древовидную структуру данных. Или графовую.
И сделайте по ней алгоритм итератора.

Обеспечте интерфейс который обходит все узлы:

Код: plaintext
1.
2.
3.
4.
interface RecursiveIterator<Node>{
  bool hasNext();
  Node next();
}



Это и есть контр пример. Тоесть пример того как кодить неудобно.

Без использования уступчивого return, мультипоточности и contnuations.

Это моё ИМХО.
...
Рейтинг: 0 / 0
Тяпничная со-программа
    #39294726
Фотография mayton
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
kealon(Ruslan)mayton,
а goto отменили что ли уже?

как то давно, когда студентом был, делал нитевую многопоточность на TurboPascal-е, в общем-то такую вещь там можно было реализовать.

каналы как абстракция делаются в любом из языков поддерживающих генерики или метапрограммирование, зачем для этого делать языковые конструкции?
Ну... я готов покорректировать табличку. Наверное в Турбо-Паскале есть языковый API для работы
с каналами. Я этого не знал.

Приведите пример плиз. Буду признателен.
...
Рейтинг: 0 / 0
Тяпничная со-программа
    #39294736
kealon(Ruslan)
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
maytonНу... я готов покорректировать табличку. Наверное в Турбо-Паскале есть языковый API для работы
с каналами. Я этого не знал.
языковых конструкций нету, я же говорю делал нитевидную многозадачность под досом
выделялась память под стэк и проца yield, которая меняла регистр SP на доступные стэки.
Сомневаюсь что так можно под виндой сделать, хотя попробовать не мешает ...

а каналы генериками в Delphi, fpc делаются довольно легко
...
Рейтинг: 0 / 0
Тяпничная со-программа
    #39294737
Фотография mayton
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
kealon(Ruslan)а каналы генериками в Delphi, fpc делаются довольно легко
Но это language features? Или нет?
...
Рейтинг: 0 / 0
Тяпничная со-программа
    #39294741
kealon(Ruslan)
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
maytonЯ щас сходу не готов дать охватывающий пример. У меня щас нет такого под рукой.

Но я могу привести контр-пример. Возьмите любую древовидную структуру данных. Или графовую.
И сделайте по ней алгоритм итератора.

Обеспечте интерфейс который обходит все узлы:

Код: plaintext
1.
2.
3.
4.
interface RecursiveIterator<Node>{
  bool hasNext();
  Node next();
}



Это и есть контр пример. Тоесть пример того как кодить неудобно.

Без использования уступчивого return, мультипоточности и contnuations.

Это моё ИМХО.
а в чем проблема указатели в стэке сохранить?

Код: pascal
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
   generic BinMapEnum<TBinNode,TItem>=class(TObject)
    public
     type
       PBinNode=^TBinNode;
    private
     Stack:TStack;
     CurrentNode:PBinNode;
     function GetCurrent: TItem;
    public
     constructor Create(const ARoot:PBinNode);
     destructor Destroy;override;

     function MoveNext: Boolean;
     property Current: TItem read GetCurrent;
   end;

{ BinMapEnum }

function BinMapEnum.GetCurrent: TItem;
begin
  Result:=CurrentNode^.Data;
end;

constructor BinMapEnum.Create(const ARoot: PBinNode);
var N:PBinNode;
begin
  inherited Create;
  N:=ARoot;
  while N<>nil do begin
    Stack.Push(N);
    N:=N^.Left;
  end;
end;

destructor BinMapEnum.Destroy;
begin
  Stack.Done;
  inherited Destroy;
end;

function BinMapEnum.MoveNext: Boolean;
begin
  if CurrentNode=nil then begin
    CurrentNode:=Stack.Pop;
  end else begin
    if CurrentNode^.Right<>nil then begin
      CurrentNode:=CurrentNode^.Right;
      While CurrentNode^.Left<>nil do begin
        Stack.Push(CurrentNode);
        CurrentNode:=CurrentNode^.Left;
      end;
    end else begin
      CurrentNode:=Stack.Pop;
    end;
  end;

  Result:=(CurrentNode<>nil);
end;


...
Рейтинг: 0 / 0
Тяпничная со-программа
    #39294743
kealon(Ruslan)
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
maytonkealon(Ruslan)а каналы генериками в Delphi, fpc делаются довольно легко
Но это language features? Или нет?
это скорее language-oldest, зачем это делать если либами можно сделать? да и с ОС проблемы, она просто так стэковые регистры менять не даёт
...
Рейтинг: 0 / 0
Тяпничная со-программа
    #39294746
Фотография mayton
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
kealon(Ruslan)
Код: pascal
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
   generic BinMapEnum<TBinNode,TItem>=class(TObject)
    public
     type
       PBinNode=^TBinNode;
    private
     Stack:TStack;
     CurrentNode:PBinNode;
     function GetCurrent: TItem;
    public
     constructor Create(const ARoot:PBinNode);
     destructor Destroy;override;

     function MoveNext: Boolean;
     property Current: TItem read GetCurrent;
   end;



Эээ... надо подумать.
...
Рейтинг: 0 / 0
Тяпничная со-программа
    #39294780
Dima T
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
yield в C# это тема, правда я в нее еще не вникал глубоко. Тут фундаментально меняется подход, ты получаешь на вход интерфейс, который будет (будет в будущем) возвращать тебе последовательность, дальше можно его передать на вход следующей функции и т.д. Это и есть основа LINQ.
И в итоге это выглядит так, например найти первое число в массиве между 1-10
Код: c#
1.
res = array.Where(x => x >= 1 && x <= 10).Single();


и тут перебор будет именно до первого удовлетворяющего условию. Синтаксический сахар конечно, но сахар он на то и нужен чтобы сладко было.
...
Рейтинг: 0 / 0
Тяпничная со-программа
    #39294785
Фотография mayton
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
Dima T, это пример вырожденный. Его легко итерациями сделать.
А вот задачка обхода деревьев как функция которая возвращает
узлы - это красиво.
...
Рейтинг: 0 / 0
Тяпничная со-программа
    #39294786
Dima T
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
maytonDima T, это пример вырожденный. Его легко итерациями сделать.
Я не спорю, я же написал что это синтаксический сахар, но я не люблю С/С++ именно за то что обязательно надо это делать, т.е. писать кучу букав, потом отлаживать, а тут написал одну строчку и получил что хотел. Мне не нравится С именно поэтому, т.к. для элементарного действия надо написать портянку на два экрана, когда в высокоуровневом ЯП это одна строка.
...
Рейтинг: 0 / 0
Тяпничная со-программа
    #39294790
Фотография mayton
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
Дима.

1) Я понял. Я думаю что ближайшие лет 10 основной темой споров будут
доводы за и против лямбд и Stram-операций в обычных ЯП.

У меня отношения к лямбдам весьма сдержанное. Тоесть я сперва
хочу понять pitfalls в части их практического использования (Java) как то
отладка, области видимости (лямбда не видит не статичные переменные класса),
и обработка исключений внутри лямбд (куда мы вываливаемся?) и
конечно-же перформанс.

Но эта тема другого топика.

2) Я вернусь к своим баранам. А именно к coroutine. Разворачивая рекурсию
в С-программе которую я привел выше я пришел к следующему итератору.

В нем полезным по сути является копи-паста С-программы а именно
рекурсивная процедура обхода квадрата по Гилберту. Остальное - шлак.
Все эти обвязки, потоки и Блокирующая очередь - просто служебные
тулзы которые я притянул за уши только чтобы достать координаты пикселов.
Есть также странные хардкоденные константы типа Position.Dummy смысл
которых - уведомить итератор о том что очередь завершена и больше данных
ждать не стоит. Вобщем много всякой фигни.

Код: java
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
public class GilbertIteratorBlockingQueue implements Iterator<Position> {

    static Logger logger = LogManager.getLogger(GilbertIteratorBlockingQueue.class);

    Producer producer;
    int size;
    BlockingQueue<Position> queue;
    Position position = Position.DUMMY;
    int state = 0;

    public GilbertIteratorBlockingQueue(int size) {
        if (size<4){
            throw new IllegalArgumentException("The dimension of 'Gilbert' curve cannot be less than 4 x 4!");
        }
        this.size = size;
        queue = new ArrayBlockingQueue<>(1024);
        producer = new Producer();
        new Thread(producer).start();
    }


    class Producer implements Runnable {

        static final int u = 1;
        int glx;
        int gly;
        int level;

        public Producer() {
            level = log2up(size);
            int clp2size = clp2(size);
            if (clp2size > size) {
                logger.warn("Warning! The real dimension of iterator's space has been extended to {}x{} pixels", clp2size, clp2size);
            }
            state = 1;
        }

        public void run() {
            try {
                moveto(0, 0);
                a(level);
                queue.put(Position.DUMMY);
            } catch (InterruptedException e) {
                logger.error(e);
            }
        }

        void linerel(int x, int y) throws InterruptedException {
            glx += x;
            gly += y;
            queue.put(new Position(glx, gly));
        }

        void moveto(int x, int y) throws InterruptedException {
            glx = x;
            gly = y;
            queue.put(new Position(glx, gly));
        }

        // Elements of curve
        void a(int i) throws InterruptedException {
            if (i > 0) {
                d(i - 1);
                linerel(+u, 0);
                a(i - 1);
                linerel(0, u);
                a(i - 1);
                linerel(-u, 0);
                c(i - 1);
            }
        }

        void b(int i) throws InterruptedException {
            if (i > 0) {
                c(i - 1);
                linerel(-u, 0);
                b(i - 1);
                linerel(0, -u);
                b(i - 1);
                linerel(u, 0);
                d(i - 1);
            }
        }

        void c(int i) throws InterruptedException {
            if (i > 0) {
                b(i - 1);
                linerel(0, -u);
                c(i - 1);
                linerel(-u, 0);
                c(i - 1);
                linerel(0, u);
                a(i - 1);
            }
        }

        void d(int i) throws InterruptedException {
            if (i > 0) {
                a(i - 1);
                linerel(0, u);
                d(i - 1);
                linerel(u, 0);
                d(i - 1);
                linerel(0, -u);
                b(i - 1);
            }
        }


    }

    @Override
    public boolean hasNext() {
        if (state == 2){
            return false;
        }
        try {
            position = queue.take();
            if (position == Position.DUMMY) {
                state = 2;
                return false;
            }
        } catch (InterruptedException e) {
            logger.error(e);
        }
        return true;
    }

    @Override
    public Position next() {
        return position;
    }
}



Меня это обескураживает и я ищу простые лаконичные механизмы
не жрущие списки памяти и в то-же время выдающие сиквенс
значений из рекурсии.

Напомню о проблеме. Мне надо было заполнить пикселами картинку
в порядке кривой Гилберта. Но я не хотел хардкорное решение.
Мне нужны были компоненты которые "чертят" спирали, Z-кривые, змейки
и кривые Гилберта и Пеано. И пока у меня не было такой компоненты
сам кодинг плаката с IP-диаграммой мне был не интересен. И мне нужен
был механизм переключений между этими "чертилками".



3) По поводу Streams (Java8). Я надеюсь они мне позволят заменить со-программы.
...
Рейтинг: 0 / 0
Тяпничная со-программа
    #39294809
kealon(Ruslan)
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
maytonМеня это обескураживает и я ищу простые лаконичные механизмы
не жрущие списки памяти и в то-же время выдающие сиквенс
значений из рекурсии.

рекурсия это и есть список памяти, ты просто фактически не хочешь о нём заботиться сам
Рекурсия кстати процентов на 20-30 медленнее (за счёт сохранения всех данных). Чем стэковый автомат в уже выделенном блоке. За простоту кода приходится платить ресурсами.
...
Рейтинг: 0 / 0
Тяпничная со-программа
    #39294813
Фотография mayton
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
kealon(Ruslan)рекурсия это и есть список памяти, ты просто фактически не хочешь о нём заботиться сам
Рекурсия кстати процентов на 20-30 медленнее (за счёт сохранения всех данных). Чем стэковый автомат в уже выделенном блоке. За простоту кода приходится платить ресурсами.
Очень часто мы (разработчики) согласны платить 20-30% (здесь я сомневаюсь надо пересчитать)
но при этом иметь простой концептуальный код.

Если yield return несет в себе нулевые накладные расходы (синхронизации нет, нет потоков, IPC,
и очередей) то я согласен и я хочу использовать этот return в противоположность разворачиванию
рекусии в конечный автомат.

Заметьте что мы с вами еще не рассматривали оценку сложности этого процесса.
...
Рейтинг: 0 / 0
Тяпничная со-программа
    #39294814
Фотография mayton
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
Я имею в виду оценку сложности процесса переписывания рекурсии в автомат со стеком.
...
Рейтинг: 0 / 0
Тяпничная со-программа
    #39294823
kealon(Ruslan)
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
maytonЕсли yield return несет в себе нулевые накладные расходы (синхронизации нет, нет потоков, IPC,
и очередей) то я согласен и я хочу использовать этот return в противоположность разворачиванию
рекусии в конечный автомат.

несёт, для них нужен отдельный блок стэка, сколько его выделить 100кб, 1Мб или больше?
траты на yield в общем случае : все регистры, связка pushad-popad довольно быстрая, но не бесплатная операция. это мы ещё не вспоминаем про регистры fpu

maytonЗаметтье что мы с вами еще не рассматривали оценку сложности этого процесса.


есть ещё вариант: если в функциональном стиле написать алгоритм, то его можно довольно просто механически разложить через комбинаторы в "автомат со стэковой памятью"
...
Рейтинг: 0 / 0
Тяпничная со-программа
    #39294944
kealon(Ruslan)
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
mayton,

вот же нашёл ты тему, всё таки получилось у меня это сделать
...
Рейтинг: 0 / 0
Тяпничная со-программа
    #39294952
Фотография mayton
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
kealon(Ruslan), спасибо. Но я не специалист в FreePascal. Вряд-ли я могу собрать это.

А вот по поводу комбинаторов и автоматов со стеком - подкинь материала.
...
Рейтинг: 0 / 0
Тяпничная со-программа
    #39294955
kealon(Ruslan)
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
maytonkealon(Ruslan), спасибо. Но я не специалист в FreePascal. Вряд-ли я могу собрать это.

А вот по поводу комбинаторов и автоматов со стеком - подкинь материала.
тут надо подумать, знаю что можно, но вот где и когда читал - подзабыл
...
Рейтинг: 0 / 0
Тяпничная со-программа
    #39295061
Фотография tchingiz
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
авторподдерживает множество входных точек (а не одну как подпрограмма),
остановку и продолжение выполнения с сохранением определённого положения.
...
Применение сопрограмм .....практиковалось
лишь в некоторых высокоуровневых языках (Simula, Modula-2)
по моему брехня.
точки входа в подпрограмму были чуть ли не везде (пл-1 и алгол), а продолжать выполнение с сохранением определенного положения -- можно благодаря static
...
Рейтинг: 0 / 0
Тяпничная со-программа
    #39295092
kealon(Ruslan)
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
tchingizточки входа в подпрограмму были чуть ли не везде (пл-1 и алгол), а продолжать выполнение с сохранением определенного положения -- можно благодаря static

Fullstack CoRoutines на основе одной переменной в общем виде повторить не получится
смотри пример с обходом бинарного дерева

рекурсию всё равно с помощью стэка придётся делать в том или ином виде
...
Рейтинг: 0 / 0
25 сообщений из 54, страница 1 из 3
Форумы / Программирование [игнор отключен] [закрыт для гостей] / Тяпничная со-программа
Найденые пользователи ...
Разблокировать пользователей ...
Читали форум (0):
Пользователи онлайн (0):
x
x
Закрыть


Просмотр
0 / 0
Close
Debug Console [Select Text]